From 78a0e215574a06b83457ba5715adf480fdb70afd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 31 Jul 2019 16:22:05 -0700 Subject: [PATCH 001/273] Version bump to 0.98.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 17e0e6752d2..eebd10f4fb9 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 = 97 +MINOR_VERSION = 98 PATCH_VERSION = "0.dev0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) From 3bd7b15b3d7db0a2d92a312b30d07b9f9897ea41 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 31 Jul 2019 23:33:12 -0400 Subject: [PATCH 002/273] bump quirks (#25618) --- homeassistant/components/zha/manifest.json | 7 ++----- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 199428302ad..88c5f171116 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,14 +5,11 @@ "documentation": "https://www.home-assistant.io/components/zha", "requirements": [ "bellows-homeassistant==0.9.0", - "zha-quirks==0.0.19", + "zha-quirks==0.0.20", "zigpy-deconz==0.2.1", "zigpy-homeassistant==0.7.0", "zigpy-xbee-homeassistant==0.4.0" ], "dependencies": [], - "codeowners": [ - "@dmulcahey", - "@adminiuga" - ] + "codeowners": ["@dmulcahey", "@adminiuga"] } diff --git a/requirements_all.txt b/requirements_all.txt index cf06f2fbe65..c6e0ca6abf1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1963,7 +1963,7 @@ zengge==0.2 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.19 +zha-quirks==0.0.20 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 From cfd600d72ea16b82739ea9c7e33cd545481fb971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 1 Aug 2019 08:45:58 +0300 Subject: [PATCH 003/273] script/test: fix tox env (#25620) --- script/test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/test b/script/test index 14fc357eb12..8c4688a4d65 100755 --- a/script/test +++ b/script/test @@ -3,4 +3,4 @@ cd "$(dirname "$0")/.." -tox -e py35 +tox -e py36 From f9b1a5225926541cd3213b726ff35599950877fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 1 Aug 2019 08:53:53 +0300 Subject: [PATCH 004/273] Move mypy settings to setup.cfg (#25611) --- mypy.ini | 16 ---------------- setup.cfg | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 16 deletions(-) delete mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 507e46b583d..00000000000 --- a/mypy.ini +++ /dev/null @@ -1,16 +0,0 @@ -[mypy] -python_version = 3.6 -check_untyped_defs = true -disallow_incomplete_defs = true -disallow_untyped_calls = true -disallow_untyped_defs = true -follow_imports = silent -ignore_missing_imports = true -no_implicit_optional = true -strict_equality = true -warn_incomplete_stub = true -warn_redundant_casts = true -warn_return_any = true -warn_unreachable = true -warn_unused_configs = true -warn_unused_ignores = true diff --git a/setup.cfg b/setup.cfg index c6ff25bb362..49f738cf969 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,3 +52,20 @@ default_section = THIRDPARTY known_first_party = homeassistant,tests forced_separate = tests combine_as_imports = true + +[mypy] +python_version = 3.6 +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_untyped_calls = true +disallow_untyped_defs = true +follow_imports = silent +ignore_missing_imports = true +no_implicit_optional = true +strict_equality = true +warn_incomplete_stub = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true From b97bef3ac8dd02c7e1a8b879e59b17c0941b4573 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 1 Aug 2019 11:41:29 +0200 Subject: [PATCH 005/273] Remove liner for editor. Not need with black --- .devcontainer/devcontainer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e70706f8af4..c85eaece8b6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -22,7 +22,6 @@ "editor.formatOnSave": true, "editor.formatOnType": true, "files.trimTrailingWhitespace": true, - "editor.rulers": [80], "terminal.integrated.shell.linux": "/bin/bash", "yaml.customTags": [ "!secret scalar", From 61c6838fa23338f228b1b9b7720ae71272ba874b Mon Sep 17 00:00:00 2001 From: tleegaard Date: Thu, 1 Aug 2019 12:24:46 +0200 Subject: [PATCH 006/273] Add support for HomeKit CO2 sensors (#25603) --- .../components/homekit_controller/const.py | 1 + .../components/homekit_controller/sensor.py | 108 +++++++++++------- .../homekit_controller/test_sensor.py | 25 ++++ 3 files changed, 95 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 924d27218a2..6aa5dc93662 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -21,6 +21,7 @@ HOMEKIT_ACCESSORY_DISPATCH = { "lock-mechanism": "lock", "contact": "binary_sensor", "motion": "binary_sensor", + "carbon-dioxide": "sensor", "humidity": "sensor", "light": "sensor", "temperature": "sensor", diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 16109f08c10..596b697bede 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -1,4 +1,6 @@ """Support for Homekit sensors.""" +from homekit.model.characteristics import CharacteristicsTypes + from homeassistant.const import TEMP_CELSIUS from . import KNOWN_DEVICES, HomeKitEntity @@ -6,39 +8,11 @@ from . import KNOWN_DEVICES, HomeKitEntity HUMIDITY_ICON = "mdi:water-percent" TEMP_C_ICON = "mdi:thermometer" BRIGHTNESS_ICON = "mdi:brightness-6" +CO2_ICON = "mdi:periodic-table-co2" UNIT_PERCENT = "%" UNIT_LUX = "lux" - - -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": - 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) +UNIT_CO2 = "ppm" class HomeKitHumiditySensor(HomeKitEntity): @@ -51,9 +25,6 @@ class HomeKitHumiditySensor(HomeKitEntity): def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - return [CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT] @property @@ -90,9 +61,6 @@ class HomeKitTemperatureSensor(HomeKitEntity): def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - return [CharacteristicsTypes.TEMPERATURE_CURRENT] @property @@ -129,9 +97,6 @@ class HomeKitLightSensor(HomeKitEntity): def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - return [CharacteristicsTypes.LIGHT_LEVEL_CURRENT] @property @@ -156,3 +121,68 @@ class HomeKitLightSensor(HomeKitEntity): def state(self): """Return the current light level in lux.""" return self._state + + +class HomeKitCarbonDioxideSensor(HomeKitEntity): + """Representation of a Homekit Carbon Dioxide sensor.""" + + def __init__(self, *args): + """Initialise the entity.""" + super().__init__(*args) + self._state = None + + def get_characteristic_types(self): + """Define the homekit characteristics the entity is tracking.""" + return [CharacteristicsTypes.CARBON_DIOXIDE_LEVEL] + + @property + def name(self): + """Return the name of the device.""" + return "{} {}".format(super().name, "CO2") + + @property + def icon(self): + """Return the sensor icon.""" + return CO2_ICON + + @property + def unit_of_measurement(self): + """Return units for the sensor.""" + return UNIT_CO2 + + def _update_carbon_dioxide_level(self, value): + self._state = value + + @property + def state(self): + """Return the current CO2 level in ppm.""" + return self._state + + +ENTITY_TYPES = { + "humidity": HomeKitHumiditySensor, + "temperature": HomeKitTemperatureSensor, + "light": HomeKitLightSensor, + "carbon-dioxide": HomeKitCarbonDioxideSensor, +} + + +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 sensors.""" + hkid = config_entry.data["AccessoryPairingID"] + conn = hass.data[KNOWN_DEVICES][hkid] + + def async_add_service(aid, service): + entity_class = ENTITY_TYPES.get(service["stype"]) + if not entity_class: + return False + info = {"aid": aid, "iid": service["iid"]} + async_add_entities([entity_class(conn, info)], True) + return True + + conn.add_listener(async_add_service) diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index e17ad2a8c73..13d844e0162 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -4,6 +4,7 @@ from tests.components.homekit_controller.common import FakeService, setup_test_c TEMPERATURE = ("temperature", "temperature.current") HUMIDITY = ("humidity", "relative-humidity.current") LIGHT_LEVEL = ("light", "light-level.current") +CARBON_DIOXIDE_LEVEL = ("carbon-dioxide", "carbon-dioxide.level") def create_temperature_sensor_service(): @@ -36,6 +37,16 @@ def create_light_level_sensor_service(): return service +def create_carbon_dioxide_level_sensor_service(): + """Define carbon dioxide level characteristics.""" + service = FakeService("public.hap.service.sensor.carbon-dioxide") + + cur_state = service.add_characteristic("carbon-dioxide.level") + cur_state.value = 0 + + return service + + async def test_temperature_sensor_read_state(hass, utcnow): """Test reading the state of a HomeKit temperature sensor accessory.""" sensor = create_temperature_sensor_service() @@ -76,3 +87,17 @@ async def test_light_level_sensor_read_state(hass, utcnow): helper.characteristics[LIGHT_LEVEL].value = 20 state = await helper.poll_and_get_state() assert state.state == "20" + + +async def test_carbon_dioxide_level_sensor_read_state(hass, utcnow): + """Test reading the state of a HomeKit carbon dioxide sensor accessory.""" + sensor = create_carbon_dioxide_level_sensor_service() + helper = await setup_test_component(hass, [sensor], suffix="co2") + + helper.characteristics[CARBON_DIOXIDE_LEVEL].value = 10 + state = await helper.poll_and_get_state() + assert state.state == "10" + + helper.characteristics[CARBON_DIOXIDE_LEVEL].value = 20 + state = await helper.poll_and_get_state() + assert state.state == "20" From 84abb57ebca125e25c27e98cc58f990481dea5e6 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 1 Aug 2019 16:37:37 +0200 Subject: [PATCH 007/273] Fix test_install_existing_package (#25627) homeassistant.util.package.install_package is not a corutine. --- tests/test_requirements.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 0c73e9f38f0..486374e3909 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -11,12 +11,7 @@ from homeassistant.requirements import ( _install, ) -from tests.common import ( - get_test_home_assistant, - MockModule, - mock_coro, - mock_integration, -) +from tests.common import get_test_home_assistant, MockModule, mock_integration class TestRequirements: @@ -77,7 +72,7 @@ class TestRequirements: async def test_install_existing_package(hass): """Test an install attempt on an existing package.""" with patch( - "homeassistant.util.package.install_package", return_value=mock_coro(True) + "homeassistant.util.package.install_package", return_value=True ) as mock_inst: assert await async_process_requirements( hass, "test_component", ["hello==1.0.0"] From ceac35797e6b3a5c7b29889c67b7a2c0ad9d9396 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 1 Aug 2019 17:22:08 +0200 Subject: [PATCH 008/273] Handle disabled devices (#25625) --- homeassistant/components/unifi/device_tracker.py | 2 +- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifi/test_device_tracker.py | 10 ++++++++++ 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index e7344692ecd..4046f5f63d2 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -304,7 +304,7 @@ class UniFiDeviceTracker(ScannerEntity): @property def available(self) -> bool: """Return if controller is available.""" - return self.controller.available + return not self.device.disabled and self.controller.available @property def device_info(self): diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index dc5e89c147e..e849fd34d25 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/unifi", "requirements": [ - "aiounifi==8" + "aiounifi==9" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index c6e0ca6abf1..5d365c9c732 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -169,7 +169,7 @@ aiopvapi==1.6.14 aioswitcher==2019.4.26 # homeassistant.components.unifi -aiounifi==8 +aiounifi==9 # homeassistant.components.wwlln aiowwlln==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2e4cfef22d..1feeab8f32d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -68,7 +68,7 @@ aionotion==1.1.0 aioswitcher==2019.4.26 # homeassistant.components.unifi -aiounifi==8 +aiounifi==9 # homeassistant.components.wwlln aiowwlln==1.0.0 diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 937de3ad631..5accbb584b4 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -203,6 +203,16 @@ async def test_tracked_devices(hass, mock_controller): device_1 = hass.states.get("device_tracker.device_1") assert device_1.state == "home" + device_1_copy = copy(DEVICE_1) + device_1_copy["disabled"] = True + mock_controller.mock_client_responses.append({}) + mock_controller.mock_device_responses.append([device_1_copy]) + await mock_controller.async_update() + await hass.async_block_till_done() + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1.state == "unavailable" + async def test_restoring_client(hass, mock_controller): """Test the update_items function with some clients.""" From 6b22dbcd0ba6c5d14964aa012ff5a90d3cb127d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 1 Aug 2019 18:30:49 +0300 Subject: [PATCH 009/273] Blacken top level *.py (#25621) * Blacken top level *.py * Tolerate double quotes too in setup.py dependency extraction --- script/check_format | 2 +- script/gen_requirements_all.py | 2 +- setup.py | 85 ++++++++++++++++------------------ 3 files changed, 42 insertions(+), 47 deletions(-) diff --git a/script/check_format b/script/check_format index ec403c723b3..bed35ec63e4 100755 --- a/script/check_format +++ b/script/check_format @@ -7,4 +7,4 @@ black \ --check \ --fast \ --quiet \ - homeassistant tests script + homeassistant tests script *.py diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 14cb165e6cf..bc3a2133607 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -215,7 +215,7 @@ def core_requirements(): """Gather core requirements out of setup.py.""" with open("setup.py") as inp: reqs_raw = re.search(r"REQUIRES = \[(.*?)\]", inp.read(), re.S).group(1) - return re.findall(r"'(.*?)'", reqs_raw) + return [x[1] for x in re.findall(r"(['\"])(.*?)\1", reqs_raw)] def gather_recursive_requirements(domain, seen=None): diff --git a/setup.py b/setup.py index 14162a86c12..5133ce9c16b 100755 --- a/setup.py +++ b/setup.py @@ -5,55 +5,54 @@ from setuptools import setup, find_packages import homeassistant.const as hass_const -PROJECT_NAME = 'Home Assistant' -PROJECT_PACKAGE_NAME = 'homeassistant' -PROJECT_LICENSE = 'Apache License 2.0' -PROJECT_AUTHOR = 'The Home Assistant Authors' -PROJECT_COPYRIGHT = ' 2013-{}, {}'.format(dt.now().year, PROJECT_AUTHOR) -PROJECT_URL = 'https://home-assistant.io/' -PROJECT_EMAIL = 'hello@home-assistant.io' +PROJECT_NAME = "Home Assistant" +PROJECT_PACKAGE_NAME = "homeassistant" +PROJECT_LICENSE = "Apache License 2.0" +PROJECT_AUTHOR = "The Home Assistant Authors" +PROJECT_COPYRIGHT = " 2013-{}, {}".format(dt.now().year, PROJECT_AUTHOR) +PROJECT_URL = "https://home-assistant.io/" +PROJECT_EMAIL = "hello@home-assistant.io" -PROJECT_GITHUB_USERNAME = 'home-assistant' -PROJECT_GITHUB_REPOSITORY = 'home-assistant' +PROJECT_GITHUB_USERNAME = "home-assistant" +PROJECT_GITHUB_REPOSITORY = "home-assistant" -PYPI_URL = 'https://pypi.python.org/pypi/{}'.format(PROJECT_PACKAGE_NAME) -GITHUB_PATH = '{}/{}'.format( - PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY) -GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH) +PYPI_URL = "https://pypi.python.org/pypi/{}".format(PROJECT_PACKAGE_NAME) +GITHUB_PATH = "{}/{}".format(PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY) +GITHUB_URL = "https://github.com/{}".format(GITHUB_PATH) -DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, hass_const.__version__) +DOWNLOAD_URL = "{}/archive/{}.zip".format(GITHUB_URL, hass_const.__version__) PROJECT_URLS = { - 'Bug Reports': '{}/issues'.format(GITHUB_URL), - 'Dev Docs': 'https://developers.home-assistant.io/', - 'Discord': 'https://discordapp.com/invite/c5DvZ4e', - 'Forum': 'https://community.home-assistant.io/', + "Bug Reports": "{}/issues".format(GITHUB_URL), + "Dev Docs": "https://developers.home-assistant.io/", + "Discord": "https://discordapp.com/invite/c5DvZ4e", + "Forum": "https://community.home-assistant.io/", } -PACKAGES = find_packages(exclude=['tests', 'tests.*']) +PACKAGES = find_packages(exclude=["tests", "tests.*"]) REQUIRES = [ - 'aiohttp==3.5.4', - 'astral==1.10.1', - 'async_timeout==3.0.1', - 'attrs==19.1.0', - 'bcrypt==3.1.7', - 'certifi>=2019.6.16', - 'importlib-metadata==0.18', - 'jinja2>=2.10.1', - 'PyJWT==1.7.1', + "aiohttp==3.5.4", + "astral==1.10.1", + "async_timeout==3.0.1", + "attrs==19.1.0", + "bcrypt==3.1.7", + "certifi>=2019.6.16", + "importlib-metadata==0.18", + "jinja2>=2.10.1", + "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. - 'cryptography==2.7', - 'pip>=8.0.3', - 'python-slugify==3.0.2', - 'pytz>=2019.01', - 'pyyaml==5.1.1', - 'requests==2.22.0', - 'ruamel.yaml==0.15.99', - 'voluptuous==0.11.5', - 'voluptuous-serialize==2.1.0', + "cryptography==2.7", + "pip>=8.0.3", + "python-slugify==3.0.2", + "pytz>=2019.01", + "pyyaml==5.1.1", + "requests==2.22.0", + "ruamel.yaml==0.15.99", + "voluptuous==0.11.5", + "voluptuous-serialize==2.1.0", ] -MIN_PY_VERSION = '.'.join(map(str, hass_const.REQUIRED_PYTHON_VER)) +MIN_PY_VERSION = ".".join(map(str, hass_const.REQUIRED_PYTHON_VER)) setup( name=PROJECT_PACKAGE_NAME, @@ -67,11 +66,7 @@ setup( include_package_data=True, zip_safe=False, install_requires=REQUIRES, - python_requires='>={}'.format(MIN_PY_VERSION), - test_suite='tests', - entry_points={ - 'console_scripts': [ - 'hass = homeassistant.__main__:main' - ] - }, + python_requires=">={}".format(MIN_PY_VERSION), + test_suite="tests", + entry_points={"console_scripts": ["hass = homeassistant.__main__:main"]}, ) From 36129af4471e2aa1713693699762374d1b5a697b Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 1 Aug 2019 16:35:19 +0100 Subject: [PATCH 010/273] Fix polling HomeKit devices with multiple services per accessory (#25629) --- homeassistant/components/homekit_controller/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index fd9c960980c..79636cea9f3 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -120,13 +120,21 @@ class HomeKitEntity(Entity): """Collect new data from bridge and update the entity state in hass.""" accessory_state = self._accessory.current_state.get(self._aid, {}) for iid, result in accessory_state.items(): + # No value so dont process this result if "value" not in result: continue + + # Unknown iid - this is probably for a sibling service that is part + # of the same physical accessory. Ignore it. + if iid not in self._char_names: + continue + # Callback to update the entity with this characteristic value char_name = escape_characteristic_name(self._char_names[iid]) update_fn = getattr(self, "_update_{}".format(char_name), None) if not update_fn: continue + # pylint: disable=not-callable update_fn(result["value"]) From c2556d90ea7629941a834a3fd969e41ba0bdcd1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 1 Aug 2019 19:22:04 +0300 Subject: [PATCH 011/273] Huawei LTE sensor unique id improvements (#25609) * Convert sensor setup to async * Improve sensor unique ids * Save some indent levels, use f-string formatting * Require getmac in tests * Fix RouterData init in tests * Make discovery_info optional in async_setup_platform signature --- .../components/huawei_lte/__init__.py | 26 ++++++++++++----- .../components/huawei_lte/manifest.json | 1 + homeassistant/components/huawei_lte/sensor.py | 28 +++++++++++++++---- requirements_all.txt | 1 + requirements_test_all.txt | 5 ++++ script/gen_requirements_all.py | 1 + tests/components/huawei_lte/test_init.py | 2 +- 7 files changed, 50 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index c68231ba0e4..51d0dc5d3a2 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -1,12 +1,15 @@ """Support for Huawei LTE routers.""" from datetime import timedelta from functools import reduce +from urllib.parse import urlparse +import ipaddress import logging import operator from typing import Any, Callable import voluptuous as vol import attr +from getmac import get_mac_address from huawei_lte_api.AuthorizedConnection import AuthorizedConnection from huawei_lte_api.Client import Client from huawei_lte_api.exceptions import ResponseErrorNotSupportedException @@ -55,6 +58,7 @@ class RouterData: """Class for router state.""" client = attr.ib() + mac = attr.ib() device_information = attr.ib(init=False, factory=dict) device_signal = attr.ib(init=False, factory=dict) monitoring_traffic_statistics = attr.ib(init=False, factory=dict) @@ -62,12 +66,6 @@ class RouterData: _subscriptions = attr.ib(init=False, factory=set) - def __attrs_post_init__(self) -> None: - """Fetch device information once, for serial number in @unique_ids.""" - self.subscribe("device_information") - self._update() - self.unsubscribe("device_information") - def __getitem__(self, path: str): """ Get value corresponding to a dotted path. @@ -148,10 +146,24 @@ def _setup_lte(hass, lte_config) -> None: username = lte_config[CONF_USERNAME] password = lte_config[CONF_PASSWORD] + # Get MAC address for use in unique ids. Being able to use something + # from the API would be nice, but all of that seems to be available only + # through authenticated calls (e.g. device_information.SerialNumber), and + # we want this available and the same when unauthenticated too. + host = urlparse(url).hostname + try: + if ipaddress.ip_address(host).version == 6: + mode = "ip6" + else: + mode = "ip" + except ValueError: + mode = "hostname" + mac = get_mac_address(**{mode: host}) + connection = AuthorizedConnection(url, username=username, password=password) client = Client(connection) - data = RouterData(client) + data = RouterData(client, mac) hass.data[DATA_KEY].data[url] = data def cleanup(event): diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index bfdc6f167aa..f31ff74c055 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -3,6 +3,7 @@ "name": "Huawei lte", "documentation": "https://www.home-assistant.io/components/huawei_lte", "requirements": [ + "getmac==0.8.1", "huawei-lte-api==1.2.0" ], "dependencies": [], diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 02ccff82c52..e72bd3aa438 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA, DEVICE_CLASS_SIGNAL_STRENGTH, ) +from homeassistant.helpers import entity_registry from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv @@ -101,7 +102,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up Huawei LTE sensor devices.""" data = hass.data[DATA_KEY].get_data(config) sensors = [] @@ -111,7 +112,24 @@ def setup_platform(hass, config, add_entities, discovery_info): data.subscribe(path) sensors.append(HuaweiLteSensor(data, path, SENSOR_META.get(path, {}))) - add_entities(sensors, True) + # Pre-0.97 unique id migration. Old ones used the device serial number + # (see comments in HuaweiLteData._setup_lte for more info), as well as + # had a bug that joined the path str with periods, not the path components, + # resulting e.g. *_device_signal.sinr to end up as + # *_d.e.v.i.c.e._.s.i.g.n.a.l...s.i.n.r + entreg = await entity_registry.async_get_registry(hass) + for entid, ent in entreg.entities.items(): + if ent.platform != "huawei_lte": + continue + for sensor in sensors: + oldsuf = ".".join(sensor.path) + if ent.unique_id.endswith(f"_{oldsuf}"): + entreg.async_update_entity(entid, new_unique_id=sensor.unique_id) + _LOGGER.debug( + "Updated entity %s unique id to %s", entid, sensor.unique_id + ) + + async_add_entities(sensors, True) def format_default(value): @@ -134,7 +152,7 @@ class HuaweiLteSensor(Entity): """Huawei LTE sensor entity.""" data = attr.ib(type=RouterData) - path = attr.ib(type=list) + path = attr.ib(type=str) meta = attr.ib(type=dict) _state = attr.ib(init=False, default=STATE_UNKNOWN) @@ -143,9 +161,7 @@ class HuaweiLteSensor(Entity): @property def unique_id(self) -> str: """Return unique ID for sensor.""" - return "{}_{}".format( - self.data["device_information.SerialNumber"], ".".join(self.path) - ) + return "{}-{}".format(self.data.mac, self.path) @property def name(self) -> str: diff --git a/requirements_all.txt b/requirements_all.txt index 5d365c9c732..ff2d33f5d77 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -537,6 +537,7 @@ georss_ign_sismologia_client==0.2 georss_qld_bushfire_alert_client==0.3 # homeassistant.components.braviatv +# homeassistant.components.huawei_lte # homeassistant.components.nmap_tracker getmac==0.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1feeab8f32d..c2139f7f4e0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -147,6 +147,11 @@ georss_ign_sismologia_client==0.2 # homeassistant.components.qld_bushfire georss_qld_bushfire_alert_client==0.3 +# homeassistant.components.braviatv +# homeassistant.components.huawei_lte +# homeassistant.components.nmap_tracker +getmac==0.8.1 + # homeassistant.components.google google-api-python-client==1.6.4 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index bc3a2133607..edf74b93793 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -73,6 +73,7 @@ TEST_REQUIREMENTS = ( "georss_generic_client", "georss_ign_sismologia_client", "georss_qld_bushfire_alert_client", + "getmac", "google-api-python-client", "gTTS-token", "ha-ffmpeg", diff --git a/tests/components/huawei_lte/test_init.py b/tests/components/huawei_lte/test_init.py index 46cec10963a..70a00b02b4e 100644 --- a/tests/components/huawei_lte/test_init.py +++ b/tests/components/huawei_lte/test_init.py @@ -9,7 +9,7 @@ from homeassistant.components import huawei_lte @pytest.fixture(autouse=True) def routerdata(): """Set up a router data for testing.""" - rd = huawei_lte.RouterData(Mock()) + rd = huawei_lte.RouterData(Mock(), "de:ad:be:ef:00:00") rd.device_information = {"SoftwareVersion": "1.0", "nested": {"foo": "bar"}} return rd From 3649a1b5e9609d9d4553b716e0258aa0a3665d8b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Aug 2019 11:52:57 -0700 Subject: [PATCH 012/273] Filter out empty results in history API (#25633) --- homeassistant/components/history/__init__.py | 6 ++++-- tests/components/history/test_init.py | 22 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 3b751b86c73..d402aceaa40 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -234,6 +234,7 @@ def states_to_json( axis correctly. """ result = defaultdict(list) + # Set all entity IDs to empty lists in result set to maintain the order if entity_ids is not None: for ent_id in entity_ids: result[ent_id] = [] @@ -253,7 +254,9 @@ def states_to_json( # Append all changes to it for ent_id, group in groupby(states, lambda state: state.entity_id): result[ent_id].extend(group) - return result + + # Filter out the empty lists if some states had 0 results. + return {key: val for key, val in result.items() if val} def get_state(hass, utc_point_in_time, entity_id, run=None): @@ -348,7 +351,6 @@ class HistoryPeriodView(HomeAssistantView): # Optionally reorder the result to respect the ordering given # by any entities explicitly included in the configuration. - if self.use_include_order: sorted_result = [] for order_entity in self.filters.included_entities: diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index ebd5991235d..68bc9c5371f 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -628,3 +628,25 @@ async def test_fetch_period_api(hass, hass_client): "/api/history/period/{}".format(dt_util.utcnow().isoformat()) ) assert response.status == 200 + + +async def test_fetch_period_api_with_include_order(hass, hass_client): + """Test the fetch period view for history.""" + await hass.async_add_job(init_recorder_component, hass) + await async_setup_component( + hass, + "history", + { + "history": { + "use_include_order": True, + "include": {"entities": ["light.kitchen"]}, + } + }, + ) + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + client = await hass_client() + response = await client.get( + "/api/history/period/{}".format(dt_util.utcnow().isoformat()), + params={"filter_entity_id": "non.existing,something.else"}, + ) + assert response.status == 200 From a398b39e12ff049375a8e2377558ebb1d24db34e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Aug 2019 12:32:48 -0700 Subject: [PATCH 013/273] Expose comfort presets as HA presets (#25491) * Expose comfort presets as HA presets * Fix bugs * Handle unavailable * log level debug on update * Lint --- homeassistant/components/ecobee/__init__.py | 2 +- homeassistant/components/ecobee/climate.py | 65 +++++++++------------ tests/components/ecobee/test_climate.py | 14 ----- 3 files changed, 29 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index e69884af59f..cb8b7436b51 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -97,7 +97,7 @@ class EcobeeData: def update(self): """Get the latest data from pyecobee.""" self.ecobee.update() - _LOGGER.info("Ecobee data updated successfully") + _LOGGER.debug("Ecobee data updated successfully") def setup(hass, config): diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 6520a3aadba..d9af0f93e11 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -88,16 +88,6 @@ PRESET_TO_ECOBEE_HOLD = { PRESET_HOLD_INDEFINITE: "indefinite", } -PRESET_MODES = [ - PRESET_NONE, - PRESET_AWAY, - PRESET_TEMPERATURE, - PRESET_HOME, - PRESET_SLEEP, - PRESET_HOLD_NEXT_TRANSITION, - PRESET_HOLD_INDEFINITE, -] - SERVICE_SET_FAN_MIN_ON_TIME = "ecobee_set_fan_min_on_time" SERVICE_RESUME_PROGRAM = "ecobee_resume_program" @@ -199,7 +189,6 @@ class Thermostat(ClimateDevice): self._name = self.thermostat["name"] self.hold_temp = hold_temp self.vacation = None - self._climate_list = self.climate_list self._operation_list = [] if self.thermostat["settings"]["heatStages"]: @@ -210,6 +199,10 @@ class Thermostat(ClimateDevice): self._operation_list.insert(0, HVAC_MODE_AUTO) self._operation_list.append(HVAC_MODE_OFF) + self._preset_modes = { + comfort["climateRef"]: comfort["name"] + for comfort in self.thermostat["program"]["climates"] + } self._fan_modes = [FAN_AUTO, FAN_ON] self.update_without_throttle = False @@ -223,6 +216,11 @@ class Thermostat(ClimateDevice): self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) + @property + def available(self): + """Return if device is available.""" + return self.thermostat["runtime"]["connected"] + @property def supported_features(self): """Return the list of supported features.""" @@ -294,15 +292,9 @@ class Thermostat(ClimateDevice): continue if event["type"] == "hold": - if event["holdClimateRef"] == "away": - if int(event["endDate"][0:4]) - int(event["startDate"][0:4]) <= 1: - # A temporary hold from away climate is a hold - return PRESET_AWAY - # A permanent hold from away climate - return PRESET_AWAY - if event["holdClimateRef"] != "": - # Any other hold based on climate - return event["holdClimateRef"] + if event["holdClimateRef"] in self._preset_modes: + return self._preset_modes[event["holdClimateRef"]] + # Any hold not based on a climate is a temp hold return PRESET_TEMPERATURE if event["type"].startswith("auto"): @@ -324,14 +316,6 @@ class Thermostat(ClimateDevice): """Return the operation modes list.""" return self._operation_list - @property - def climate_mode(self): - """Return current mode, as the user-visible name.""" - cur = self.thermostat["program"]["currentClimateRef"] - climates = self.thermostat["program"]["climates"] - current = list(filter(lambda x: x["climateRef"] == cur, climates)) - return current[0]["name"] - @property def current_humidity(self) -> Optional[int]: """Return the current humidity.""" @@ -373,9 +357,7 @@ class Thermostat(ClimateDevice): status = self.thermostat["equipmentStatus"] return { "fan": self.fan, - "climate_mode": self.climate_mode, "equipment_running": status, - "climate_list": self.climate_list, "fan_min_on_time": self.thermostat["settings"]["fanMinOnTime"], } @@ -413,6 +395,21 @@ class Thermostat(ClimateDevice): elif preset_mode == PRESET_NONE: self.data.ecobee.resume_program(self.thermostat_index) + elif preset_mode in self.preset_modes: + climate_ref = None + + for comfort in self.thermostat["program"]["climates"]: + if comfort["name"] == preset_mode: + climate_ref = comfort["climateRef"] + break + + if climate_ref is not None: + self.data.ecobee.set_climate_hold( + self.thermostat_index, climate_ref, self.hold_preference() + ) + else: + _LOGGER.warning("Received unknown preset mode: %s", preset_mode) + else: self.data.ecobee.set_climate_hold( self.thermostat_index, preset_mode, self.hold_preference() @@ -421,7 +418,7 @@ class Thermostat(ClimateDevice): @property def preset_modes(self): """Return available preset modes.""" - return PRESET_MODES + return list(self._preset_modes.values()) def set_auto_temp_hold(self, heat_temp, cool_temp): """Set temperature hold in auto mode.""" @@ -543,9 +540,3 @@ class Thermostat(ClimateDevice): # supported; note that this should not include 'indefinite' # as an indefinite away hold is interpreted as away_mode return "nextTransition" - - @property - def climate_list(self): - """Return the list of climates currently available.""" - climates = self.thermostat["program"]["climates"] - return list(map((lambda x: x["name"]), climates)) diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index fa3f84b4b12..24938e52621 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -130,44 +130,34 @@ class TestEcobee(unittest.TestCase): """Test device state attributes property.""" self.ecobee["equipmentStatus"] = "heatPump2" assert { - "climate_list": ["Climate1", "Climate2"], "fan": "off", "fan_min_on_time": 10, - "climate_mode": "Climate1", "equipment_running": "heatPump2", } == self.thermostat.device_state_attributes self.ecobee["equipmentStatus"] = "auxHeat2" assert { - "climate_list": ["Climate1", "Climate2"], "fan": "off", "fan_min_on_time": 10, - "climate_mode": "Climate1", "equipment_running": "auxHeat2", } == self.thermostat.device_state_attributes self.ecobee["equipmentStatus"] = "compCool1" assert { - "climate_list": ["Climate1", "Climate2"], "fan": "off", "fan_min_on_time": 10, - "climate_mode": "Climate1", "equipment_running": "compCool1", } == self.thermostat.device_state_attributes self.ecobee["equipmentStatus"] = "" assert { - "climate_list": ["Climate1", "Climate2"], "fan": "off", "fan_min_on_time": 10, - "climate_mode": "Climate1", "equipment_running": "", } == self.thermostat.device_state_attributes self.ecobee["equipmentStatus"] = "Unknown" assert { - "climate_list": ["Climate1", "Climate2"], "fan": "off", "fan_min_on_time": 10, - "climate_mode": "Climate1", "equipment_running": "Unknown", } == self.thermostat.device_state_attributes @@ -267,10 +257,6 @@ class TestEcobee(unittest.TestCase): self.ecobee["settings"]["holdAction"] = action assert "nextTransition" == self.thermostat.hold_preference() - def test_climate_list(self): - """Test climate list property.""" - assert ["Climate1", "Climate2"] == self.thermostat.climate_list - def test_set_fan_mode_on(self): """Test set fan mode to on.""" self.data.reset_mock() From 767b8e9f25affbebb98d65f57c0d78b532a951ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 1 Aug 2019 22:44:02 +0300 Subject: [PATCH 014/273] Add some debugging to azure mypy job (#25632) --- azure-pipelines-ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 9158d8045f0..eef4731c4ac 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -201,15 +201,19 @@ stages: container: $[ variables['PythonMain'] ] steps: - script: | + set -x python -m venv venv . venv/bin/activate pip install -r requirements_test.txt -c homeassistant/package_constraints.txt displayName: 'Setup Env' - script: | + set -x TYPING_FILES=$(cat mypyrc) echo -e "Run mypy on: \n$TYPING_FILES" . venv/bin/activate + python --version + mypy --version mypy $TYPING_FILES displayName: 'Run mypy' From a177ef02e3d6395bc1b5a1306ac6ea1435ce2e0b Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 1 Aug 2019 20:44:30 +0100 Subject: [PATCH 015/273] Bump homekit_python to 0.15 (#25631) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 62dbf3740a3..70f6f6a3ce4 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/homekit_controller", "requirements": [ - "homekit[IP]==0.14.0" + "homekit[IP]==0.15.0" ], "dependencies": [], "zeroconf": ["_hap._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index ff2d33f5d77..5de0b412702 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -629,7 +629,7 @@ home-assistant-frontend==20190731.0 homeassistant-pyozw==0.1.4 # homeassistant.components.homekit_controller -homekit[IP]==0.14.0 +homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud homematicip==0.10.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2139f7f4e0..146d497319d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -177,7 +177,7 @@ holidays==0.9.11 home-assistant-frontend==20190731.0 # homeassistant.components.homekit_controller -homekit[IP]==0.14.0 +homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud homematicip==0.10.9 From f7a47c6cabf5c1a1143df15cb455369494a14567 Mon Sep 17 00:00:00 2001 From: Oncleben31 Date: Thu, 1 Aug 2019 21:45:16 +0200 Subject: [PATCH 016/273] Meteofrance improve log error messages (#25630) * Improve log error messages * remove unique_id not ready yet --- homeassistant/components/meteo_france/__init__.py | 14 +++++++++++--- homeassistant/components/meteo_france/sensor.py | 12 ++++++++---- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index d227a7fe47c..ab3ec45867b 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -113,13 +113,17 @@ def setup(hass, config): # If weather alert monitoring is expected initiate a client to be used by # all weather_alert entities. if need_weather_alert_watcher: + _LOGGER.debug("Weather Alert monitoring expected. Loading vigilancemeteo") from vigilancemeteo import VigilanceMeteoFranceProxy, VigilanceMeteoError weather_alert_client = VigilanceMeteoFranceProxy() try: weather_alert_client.update_data() except VigilanceMeteoError as exp: - _LOGGER.error(exp) + _LOGGER.error( + "Unexpected error when creating the" "vigilance_meteoFrance proxy: %s ", + exp, + ) else: weather_alert_client = None hass.data[DATA_METEO_FRANCE]["weather_alert_client"] = weather_alert_client @@ -133,7 +137,9 @@ def setup(hass, config): try: client = meteofranceClient(city) except meteofranceError as exp: - _LOGGER.error(exp) + _LOGGER.error( + "Unexpected error when creating the meteofrance proxy: %s", exp + ) return client.need_rain_forecast = bool( @@ -179,4 +185,6 @@ class MeteoFranceUpdater: try: self._client.update() except meteofranceError as exp: - _LOGGER.error(exp) + _LOGGER.error( + "Unexpected error when updating the meteofrance proxy: %s", exp + ) diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 9ee9ce9cef6..95113a60cd3 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -35,18 +35,22 @@ def setup_platform(hass, config, add_entities, discovery_info=None): datas["dept"], weather_alert_client ) except ValueError as exp: - _LOGGER.error(exp) + _LOGGER.error( + "Unexpected error when creating the weather alert sensor for %s in department %s: %s", + city, + datas["dept"], + exp, + ) alert_watcher = None else: _LOGGER.info( - "weather alert watcher added for %s" "in department %s", + "Weather alert watcher added for %s" "in department %s", city, datas["dept"], ) else: _LOGGER.warning( - "No dept key found for '%s'. So weather alert " - "information won't be available", + "No 'dept' key found for '%s'. So weather alert information won't be available", city, ) # Exit and don't create the sensor if no department code available. From c3cdd3e7d2c55077c297f8bbc4c5cba30f7d3ec2 Mon Sep 17 00:00:00 2001 From: Martin Eberhardt Date: Thu, 1 Aug 2019 22:02:11 +0200 Subject: [PATCH 017/273] Fix handling of empty results from Rejseplanen (#25610) * Improve handling of empty results from Rejseplanen (Fixes #25566) * Exclude attributes with null value * Add period back into docstring * Fix formatting --- .../components/rejseplanen/sensor.py | 37 ++++++------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py index 3ba2b06eb02..99cfe1067e8 100755 --- a/homeassistant/components/rejseplanen/sensor.py +++ b/homeassistant/components/rejseplanen/sensor.py @@ -111,14 +111,14 @@ class RejseplanenTransportSensor(Entity): def device_state_attributes(self): """Return the state attributes.""" if not self._times: - return None + return {ATTR_STOP_ID: self._stop_id, ATTR_ATTRIBUTION: ATTRIBUTION} next_up = [] if len(self._times) > 1: next_up = self._times[1:] - params = { - ATTR_DUE_IN: str(self._times[0][ATTR_DUE_IN]), + return { + ATTR_DUE_IN: self._times[0][ATTR_DUE_IN], ATTR_DUE_AT: self._times[0][ATTR_DUE_AT], ATTR_TYPE: self._times[0][ATTR_TYPE], ATTR_ROUTE: self._times[0][ATTR_ROUTE], @@ -128,7 +128,6 @@ class RejseplanenTransportSensor(Entity): ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_NEXT_UP: next_up, } - return {k: v for k, v in params.items() if v} @property def unit_of_measurement(self): @@ -144,10 +143,14 @@ class RejseplanenTransportSensor(Entity): """Get the latest data from rejseplanen.dk and update the states.""" self.data.update() self._times = self.data.info - try: - self._state = self._times[0][ATTR_DUE_IN] - except TypeError: - pass + + if not self._times: + self._state = None + else: + try: + self._state = self._times[0][ATTR_DUE_IN] + except TypeError: + pass class PublicTransportData: @@ -159,20 +162,7 @@ class PublicTransportData: self.route = route self.direction = direction self.departure_type = departure_type - self.info = self.empty_result() - - def empty_result(self): - """Object returned when no departures are found.""" - return [ - { - ATTR_DUE_IN: "n/a", - ATTR_DUE_AT: "n/a", - ATTR_TYPE: "n/a", - ATTR_ROUTE: self.route, - ATTR_DIRECTION: "n/a", - ATTR_STOP_NAME: "n/a", - } - ] + self.info = [] def update(self): """Get the latest data from rejseplanen.""" @@ -200,11 +190,9 @@ class PublicTransportData: ) except rjpl.rjplAPIError as error: _LOGGER.debug("API returned error: %s", error) - self.info = self.empty_result() return except (rjpl.rjplConnectionError, rjpl.rjplHTTPError): _LOGGER.debug("Error occured while connecting to the API") - self.info = self.empty_result() return # Filter result @@ -246,7 +234,6 @@ class PublicTransportData: if not self.info: _LOGGER.debug("No departures with given parameters") - self.info = self.empty_result() # Sort the data by time self.info = sorted(self.info, key=itemgetter(ATTR_DUE_IN)) From 35400b0db1ab3ced3e9e7e8034450981f01486b4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Aug 2019 13:03:45 -0700 Subject: [PATCH 018/273] Upgrade hass-nabucasa to 0.16 (#25636) --- homeassistant/components/cloud/manifest.json | 13 +++---------- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index e848f54425b..58739bededc 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,14 +2,7 @@ "domain": "cloud", "name": "Cloud", "documentation": "https://www.home-assistant.io/components/cloud", - "requirements": [ - "hass-nabucasa==0.15" - ], - "dependencies": [ - "http", - "webhook" - ], - "codeowners": [ - "@home-assistant/cloud" - ] + "requirements": ["hass-nabucasa==0.16"], + "dependencies": ["http", "webhook"], + "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 187ee921e25..c5c8dd9c9c0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -9,7 +9,7 @@ bcrypt==3.1.7 certifi>=2019.6.16 cryptography==2.7 distro==1.4.0 -hass-nabucasa==0.15 +hass-nabucasa==0.16 home-assistant-frontend==20190731.0 importlib-metadata==0.18 jinja2>=2.10.1 diff --git a/requirements_all.txt b/requirements_all.txt index 5de0b412702..82630107fcf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -593,7 +593,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.15 +hass-nabucasa==0.16 # homeassistant.components.mqtt hbmqtt==0.9.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 146d497319d..81ab176514a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -162,7 +162,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.15 +hass-nabucasa==0.16 # homeassistant.components.mqtt hbmqtt==0.9.4 From 87bc2134adfd5523e42cc12f69b80e0ba0dba1fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Thu, 1 Aug 2019 22:18:52 +0200 Subject: [PATCH 019/273] Add each fronius sensor as own template (#25608) * Fix formatting in history test * Add each sensor as own template * Make adapters non-entities * Externalize and organize data fetching, improve system view * Small fixes Rename fetching adapters to adapters throw away non-working system overviews slightly change naming remove scan_interval from schema formatting * Scan interval is already timedelta and unnecessary return * Formatting * Ensure better codestyle by storing cell variables explicitely in different places --- homeassistant/components/fronius/sensor.py | 188 +++++++++++++++------ 1 file changed, 136 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index ec1922e0d56..ff0694afaab 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -1,5 +1,6 @@ """Support for Fronius devices.""" import copy +from datetime import timedelta import logging import voluptuous as vol @@ -11,10 +12,13 @@ from homeassistant.const import ( CONF_SENSOR_TYPE, CONF_DEVICE, CONF_MONITORED_CONDITIONS, + CONF_SCAN_INTERVAL, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval + _LOGGER = logging.getLogger(__name__) @@ -30,6 +34,7 @@ SCOPE_SYSTEM = "system" DEFAULT_SCOPE = SCOPE_DEVICE DEFAULT_DEVICE = 0 DEFAULT_INVERTER = 1 +DEFAULT_SCAN_INTERVAL = timedelta(seconds=60) SENSOR_TYPES = [TYPE_INVERTER, TYPE_STORAGE, TYPE_METER, TYPE_POWER_FLOW] SCOPE_TYPES = [SCOPE_DEVICE, SCOPE_SYSTEM] @@ -78,47 +83,64 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= session = async_get_clientsession(hass) fronius = Fronius(session, config[CONF_RESOURCE]) - sensors = [] + scan_interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + adapters = [] + # Creates all adapters for monitored conditions for condition in config[CONF_MONITORED_CONDITIONS]: device = condition[CONF_DEVICE] - name = "Fronius {} {} {}".format( - condition[CONF_SENSOR_TYPE].replace("_", " ").capitalize(), - device, - config[CONF_RESOURCE], - ) sensor_type = condition[CONF_SENSOR_TYPE] scope = condition[CONF_SCOPE] + name = "Fronius {} {} {}".format( + condition[CONF_SENSOR_TYPE].replace("_", " ").capitalize(), + device if scope == SCOPE_DEVICE else SCOPE_SYSTEM, + config[CONF_RESOURCE], + ) if sensor_type == TYPE_INVERTER: if scope == SCOPE_SYSTEM: - sensor_cls = FroniusInverterSystem + adapter_cls = FroniusInverterSystem else: - sensor_cls = FroniusInverterDevice + adapter_cls = FroniusInverterDevice elif sensor_type == TYPE_METER: if scope == SCOPE_SYSTEM: - sensor_cls = FroniusMeterSystem + adapter_cls = FroniusMeterSystem else: - sensor_cls = FroniusMeterDevice + adapter_cls = FroniusMeterDevice elif sensor_type == TYPE_POWER_FLOW: - sensor_cls = FroniusPowerFlow + adapter_cls = FroniusPowerFlow else: - sensor_cls = FroniusStorage + adapter_cls = FroniusStorage - sensors.append(sensor_cls(fronius, name, device)) + adapters.append(adapter_cls(fronius, name, device, async_add_entities)) - async_add_entities(sensors, True) + # Creates a lamdba that fetches an update when called + def adapter_data_fetcher(data_adapter): + async def fetch_data(*_): + await data_adapter.async_update() + + return fetch_data + + # Set up the fetching in a fixed interval for each adapter + for adapter in adapters: + fetch = adapter_data_fetcher(adapter) + # fetch data once at set-up + await fetch() + async_track_time_interval(hass, fetch, scan_interval) -class FroniusSensor(Entity): - """The Fronius sensor implementation.""" +class FroniusAdapter: + """The Fronius sensor fetching component.""" - def __init__(self, data, name, device): + def __init__(self, bridge, name, device, add_entities): """Initialize the sensor.""" - self.data = data + self.bridge = bridge self._name = name self._device = device - self._state = None - self._attributes = {} + self._fetched = {} + + self.sensors = set() + self._registered_sensors = set() + self._add_entities = add_entities @property def name(self): @@ -126,14 +148,9 @@ class FroniusSensor(Entity): return self._name @property - def state(self): - """Return the current state.""" - return self._state - - @property - def device_state_attributes(self): + def data(self): """Return the state attributes.""" - return self._attributes + return self._fetched async def async_update(self): """Retrieve and update latest state.""" @@ -148,62 +165,129 @@ class FroniusSensor(Entity): "Maybe the configured device is not supported" ) - if values: - self._state = values["status"]["Code"] - attributes = {} - for key in values: - if "value" in values[key]: - attributes[key] = values[key].get("value", 0) - self._attributes = attributes + if not values: + return + attributes = self._fetched + # Copy data of current fronius device + for key, entry in values.items(): + # If the data is directly a sensor + if "value" in entry: + attributes[key] = entry + self._fetched = attributes + + # Add discovered value fields as sensors + # because some fields are only sent temporarily + new_sensors = [] + for key in attributes: + if key not in self.sensors: + self.sensors.add(key) + _LOGGER.info("Discovered %s, adding as sensor", key) + new_sensors.append(FroniusTemplateSensor(self, key)) + self._add_entities(new_sensors, True) + + # Schedule an update for all included sensors + for sensor in self._registered_sensors: + sensor.async_schedule_update_ha_state(True) async def _update(self): """Return values of interest.""" pass + async def register(self, sensor): + """Register child sensor for update subscriptions.""" + self._registered_sensors.add(sensor) -class FroniusInverterSystem(FroniusSensor): - """Sensor for the fronius inverter with system scope.""" + +class FroniusInverterSystem(FroniusAdapter): + """Adapter for the fronius inverter with system scope.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_system_inverter_data() + return await self.bridge.current_system_inverter_data() -class FroniusInverterDevice(FroniusSensor): - """Sensor for the fronius inverter with device scope.""" +class FroniusInverterDevice(FroniusAdapter): + """Adapter for the fronius inverter with device scope.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_inverter_data(self._device) + return await self.bridge.current_inverter_data(self._device) -class FroniusStorage(FroniusSensor): - """Sensor for the fronius battery storage.""" +class FroniusStorage(FroniusAdapter): + """Adapter for the fronius battery storage.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_storage_data(self._device) + return await self.bridge.current_storage_data(self._device) -class FroniusMeterSystem(FroniusSensor): - """Sensor for the fronius meter with system scope.""" +class FroniusMeterSystem(FroniusAdapter): + """Adapter for the fronius meter with system scope.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_system_meter_data() + return await self.bridge.current_system_meter_data() -class FroniusMeterDevice(FroniusSensor): - """Sensor for the fronius meter with device scope.""" +class FroniusMeterDevice(FroniusAdapter): + """Adapter for the fronius meter with device scope.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_meter_data(self._device) + return await self.bridge.current_meter_data(self._device) -class FroniusPowerFlow(FroniusSensor): - """Sensor for the fronius power flow.""" +class FroniusPowerFlow(FroniusAdapter): + """Adapter for the fronius power flow.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_power_flow() + return await self.bridge.current_power_flow() + + +class FroniusTemplateSensor(Entity): + """Sensor for the single values (e.g. pv power, ac power).""" + + def __init__(self, parent: FroniusAdapter, name): + """Initialize a singular value sensor.""" + self._name = name + self.parent = parent + self._state = None + self._unit = None + + @property + def name(self): + """Return the name of the sensor.""" + return "{} {}".format( + self._name.replace("_", " ").capitalize(), self.parent.name + ) + + @property + def state(self): + """Return the current state.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + @property + def should_poll(self): + """Device should not be polled, returns False.""" + return False + + async def async_update(self): + """Update the internal state.""" + state = self.parent.data.get(self._name) + self._state = state.get("value") + self._unit = state.get("unit") + + async def async_added_to_hass(self): + """Register at parent component for updates.""" + await self.parent.register(self) + + def __hash__(self): + """Hash sensor by hashing its name.""" + return hash(self.name) From 9f3e388b04e7a740a2ce83c9bbaeeaf1edca73cc Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 1 Aug 2019 22:22:57 +0200 Subject: [PATCH 020/273] Add Plugwise component (#25533) * Added Plugwise component * pylint fixes * Additional pylints (local tox not noticing these) * Changes according to review * CI flake8 correction * Applying Black * Review changes, without exception and schema * Review changes, exception improvement * Review changes, exception correctio and schema cleanup * Further cleaning as per review * Removed blank line * Add debugging to API init * Add debugging to API init without dot --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/plugwise/__init__.py | 1 + homeassistant/components/plugwise/climate.py | 213 ++++++++++++++++++ .../components/plugwise/manifest.json | 8 + requirements_all.txt | 3 + 6 files changed, 227 insertions(+) create mode 100644 homeassistant/components/plugwise/__init__.py create mode 100644 homeassistant/components/plugwise/climate.py create mode 100644 homeassistant/components/plugwise/manifest.json diff --git a/.coveragerc b/.coveragerc index 7f3aa2762bd..abd29a8614b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -467,6 +467,7 @@ omit = homeassistant/components/plaato/* homeassistant/components/plex/media_player.py homeassistant/components/plex/sensor.py + homeassistant/components/plugwise/* homeassistant/components/plum_lightpad/* homeassistant/components/pocketcasts/sensor.py homeassistant/components/point/* diff --git a/CODEOWNERS b/CODEOWNERS index c5ff9a21b4b..232bbc4b07b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -203,6 +203,7 @@ homeassistant/components/philips_js/* @elupus homeassistant/components/pi_hole/* @fabaff homeassistant/components/plaato/* @JohNan homeassistant/components/plant/* @ChristianKuehnel +homeassistant/components/plugwise/* @laetificat @CoMPaTech homeassistant/components/point/* @fredrike homeassistant/components/ps4/* @ktnrg45 homeassistant/components/ptvsd/* @swamp-ig diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py new file mode 100644 index 00000000000..afde7eae5d6 --- /dev/null +++ b/homeassistant/components/plugwise/__init__.py @@ -0,0 +1 @@ +"""Plugwise Climate (current only Anna) component for HomeAssistant.""" diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py new file mode 100644 index 00000000000..b3b4cf3c1d4 --- /dev/null +++ b/homeassistant/components/plugwise/climate.py @@ -0,0 +1,213 @@ +"""Plugwise Climate component for HomeAssistant.""" + +import logging + +import voluptuous as vol +import haanna + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + HVAC_MODE_AUTO, + HVAC_MODE_OFF, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + TEMP_CELSIUS, +) +from homeassistant.exceptions import PlatformNotReady + +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE + +_LOGGER = logging.getLogger(__name__) + +# Configuration directives +CONF_MIN_TEMP = "min_temp" +CONF_MAX_TEMP = "max_temp" + +# Default directives +DEFAULT_NAME = "Plugwise Thermostat" +DEFAULT_USERNAME = "smile" +DEFAULT_TIMEOUT = 10 +DEFAULT_PORT = 80 +DEFAULT_ICON = "mdi:thermometer" +DEFAULT_MIN_TEMP = 4 +DEFAULT_MAX_TEMP = 30 + +# HVAC modes +ATTR_HVAC_MODES = [HVAC_MODE_AUTO, HVAC_MODE_OFF] + +# Read platform configuration +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): cv.positive_int, + vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): cv.positive_int, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Add the Plugwise (Anna) Thermostate.""" + api = haanna.Haanna( + config[CONF_USERNAME], + config[CONF_PASSWORD], + config[CONF_HOST], + config[CONF_PORT], + ) + try: + api.ping_anna_thermostat() + except OSError: + _LOGGER.debug("Ping failed, retrying later", exc_info=True) + raise PlatformNotReady + devices = [ + ThermostatDevice( + api, config[CONF_NAME], config[CONF_MIN_TEMP], config[CONF_MAX_TEMP] + ) + ] + add_entities(devices, True) + + +class ThermostatDevice(ClimateDevice): + """Representation of an Plugwise thermostat.""" + + def __init__(self, api, name, min_temp, max_temp): + """Set up the Plugwise API.""" + self._api = api + self._min_temp = min_temp + self._max_temp = max_temp + self._name = name + self._domain_objects = None + self._outdoor_temperature = None + self._active_schema = None + self._preset_mode = None + self._hvac_modes = ATTR_HVAC_MODES + + @property + def hvac_action(self): + """Return the current action.""" + if self._api.get_heating_status(self._domain_objects): + return CURRENT_HVAC_HEAT + return CURRENT_HVAC_IDLE + + @property + def name(self): + """Return the name of the thermostat, if any.""" + return self._name + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return DEFAULT_ICON + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + attributes = {} + attributes["outdoor_temperature"] = self._outdoor_temperature + attributes["available_schemas"] = self._api.get_schema_names( + self._domain_objects + ) + attributes["active_schema"] = self._active_schema + return attributes + + def update(self): + """Update the data from the thermostat.""" + _LOGGER.debug("Update called") + self._domain_objects = self._api.get_domain_objects() + self._outdoor_temperature = self._api.get_outdoor_temperature( + self._domain_objects + ) + self._active_schema = self._api.get_active_schema_name(self._domain_objects) + + @property + def hvac_mode(self): + """Return current active hvac state.""" + if self._api.get_schema_state(self._domain_objects): + return HVAC_MODE_AUTO + return HVAC_MODE_OFF + + @property + def preset_mode(self): + """Return the active preset mode.""" + return self._api.get_current_preset(self._domain_objects) + + @property + def preset_modes(self): + """Return the available preset modes list without values.""" + presets = list(self._api.get_presets(self._domain_objects)) + return presets + + @property + def hvac_modes(self): + """Return the available hvac modes list.""" + return self._hvac_modes + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._api.get_temperature(self._domain_objects) + + @property + def min_temp(self): + """Return the minimal temperature possible to set.""" + return self._min_temp + + @property + def max_temp(self): + """Return the maximum temperature possible to set.""" + return self._max_temp + + @property + def target_temperature(self): + """Return the target temperature.""" + return self._api.get_target_temperature(self._domain_objects) + + @property + def temperature_unit(self): + """Return the unit of measured temperature.""" + return TEMP_CELSIUS + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + _LOGGER.debug("Adjusting temperature") + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is not None and self._min_temp < temperature < self._max_temp: + _LOGGER.debug("Changing temporary temperature") + self._api.set_temperature(self._domain_objects, temperature) + else: + _LOGGER.error("Invalid temperature requested") + + def set_hvac_mode(self, hvac_mode): + """Set the hvac mode.""" + _LOGGER.debug("Adjusting hvac_mode (i.e. schedule/schema)") + schema_mode = "false" + if hvac_mode == HVAC_MODE_AUTO: + schema_mode = "true" + self._api.set_schema_state( + self._domain_objects, self._active_schema, schema_mode + ) + + def set_preset_mode(self, preset_mode): + """Set the preset mode.""" + _LOGGER.debug("Changing preset mode") + self._preset_mode = preset_mode + self._api.set_preset(self._domain_objects, preset_mode) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json new file mode 100644 index 00000000000..3a5f4ae0be4 --- /dev/null +++ b/homeassistant/components/plugwise/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "plugwise", + "name": "Plugwise", + "documentation": "https://www.home-assistant.io/components/plugwise", + "dependencies": [], + "codeowners": ["@laetificat","@CoMPaTech"], + "requirements": ["haanna==0.10.0"] +} diff --git a/requirements_all.txt b/requirements_all.txt index 82630107fcf..4b40bfef566 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -586,6 +586,9 @@ ha-ffmpeg==2.0 # homeassistant.components.philips_js ha-philipsjs==0.0.8 +# homeassistant.components.plugwise +haanna==0.10.0 + # homeassistant.components.habitica habitipy==0.2.0 From f4fce8fcc31e9751d2fbdca44a64e10ee1e35f10 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Aug 2019 13:34:28 -0700 Subject: [PATCH 021/273] Updated frontend to 20190801.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index c3b7fa3e392..b6a996afc98 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==20190731.0" + "home-assistant-frontend==20190801.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c5c8dd9c9c0..942cdb577f9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2019.6.16 cryptography==2.7 distro==1.4.0 hass-nabucasa==0.16 -home-assistant-frontend==20190731.0 +home-assistant-frontend==20190801.0 importlib-metadata==0.18 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4b40bfef566..6352011ab23 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -626,7 +626,7 @@ hole==0.3.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190731.0 +home-assistant-frontend==20190801.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 81ab176514a..2c65f9c063a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -174,7 +174,7 @@ hdate==0.8.8 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190731.0 +home-assistant-frontend==20190801.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From b5e296238fe7330392dd784a50af822fb1a5aa48 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Aug 2019 16:32:43 -0700 Subject: [PATCH 022/273] Add preset to be away and eco (#25643) --- homeassistant/components/nest/climate.py | 27 ++++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index ac13f2b004f..dc4b0bd33ae 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -61,7 +61,9 @@ ACTION_NEST_TO_HASS = { "cooling": CURRENT_HVAC_COOL, } -PRESET_MODES = [PRESET_NONE, PRESET_AWAY, PRESET_ECO] +PRESET_AWAY_AND_ECO = "Away and Eco" + +PRESET_MODES = [PRESET_NONE, PRESET_AWAY, PRESET_ECO, PRESET_AWAY_AND_ECO] def setup_platform(hass, config, add_entities, discovery_info=None): @@ -259,6 +261,9 @@ class NestThermostat(ClimateDevice): @property def preset_mode(self): """Return current preset mode.""" + if self._away and self._mode == NEST_MODE_ECO: + return PRESET_AWAY_AND_ECO + if self._away: return PRESET_AWAY @@ -277,15 +282,19 @@ class NestThermostat(ClimateDevice): if preset_mode == self.preset_mode: return - if self._away: - self.structure.away = False - elif preset_mode == PRESET_AWAY: - self.structure.away = True + need_away = preset_mode in (PRESET_AWAY, PRESET_AWAY_AND_ECO) + need_eco = preset_mode in (PRESET_ECO, PRESET_AWAY_AND_ECO) + is_away = self._away + is_eco = self._mode == NEST_MODE_ECO - if self.preset_mode == PRESET_ECO: - self.device.mode = MODE_HASS_TO_NEST[self._operation_list[0]] - elif preset_mode == PRESET_ECO: - self.device.mode = NEST_MODE_ECO + if is_away != need_away: + self.structure.away = need_away + + if is_eco != need_eco: + if need_eco: + self.device.mode = NEST_MODE_ECO + else: + self.device.mode = MODE_HASS_TO_NEST[self._operation_list[0]] @property def fan_mode(self): From 8a572557964d80652b4b2f02f895d5ca8b0a4f3f Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 2 Aug 2019 01:43:08 +0200 Subject: [PATCH 023/273] Add HmIP-SCI to Homematic IP Cloud, Fix HmIP-SWDM (#25639) * Add HmIP-SCI to Homematic IP Cloud * Bump upstream dependency * Fix HmIP-SWDM --- .../components/homematicip_cloud/binary_sensor.py | 9 +++++++-- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 9580b803596..7bb7718f0b3 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -2,6 +2,7 @@ import logging from homematicip.aio.device import ( + AsyncContactInterface, AsyncDevice, AsyncFullFlushContactInterface, AsyncMotionDetectorIndoor, @@ -10,6 +11,7 @@ from homematicip.aio.device import ( AsyncPresenceDetectorIndoor, AsyncRotaryHandleSensor, AsyncShutterContact, + AsyncShutterContactMagnetic, AsyncSmokeDetector, AsyncWaterSensor, AsyncWeatherSensor, @@ -63,9 +65,12 @@ async def async_setup_entry( home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: - if isinstance(device, AsyncFullFlushContactInterface): + if isinstance(device, (AsyncContactInterface, AsyncFullFlushContactInterface)): devices.append(HomematicipContactInterface(home, device)) - if isinstance(device, (AsyncShutterContact, AsyncRotaryHandleSensor)): + if isinstance( + device, + (AsyncShutterContact, AsyncShutterContactMagnetic, AsyncRotaryHandleSensor), + ): devices.append(HomematicipShutterContact(home, device)) if isinstance( device, diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index b679130ce05..ee0d2cb1271 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/homematicip_cloud", "requirements": [ - "homematicip==0.10.9" + "homematicip==0.10.10" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 6352011ab23..28e702ee31d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -635,7 +635,7 @@ homeassistant-pyozw==0.1.4 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.9 +homematicip==0.10.10 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2c65f9c063a..6549e9964ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -180,7 +180,7 @@ home-assistant-frontend==20190801.0 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.9 +homematicip==0.10.10 # homeassistant.components.google # homeassistant.components.remember_the_milk From 7ee3dd137bc9646e169d121ace72715013b344f0 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Fri, 2 Aug 2019 09:49:24 +0200 Subject: [PATCH 024/273] Fix velbus codeowner (#25649) --- CODEOWNERS | 2 +- homeassistant/components/velbus/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 232bbc4b07b..3195ca3ac98 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -287,7 +287,7 @@ homeassistant/components/updater/* @home-assistant/core homeassistant/components/upnp/* @robbiet480 homeassistant/components/uptimerobot/* @ludeeus homeassistant/components/utility_meter/* @dgomes -homeassistant/components/velbus/* @ceral2nd +homeassistant/components/velbus/* @cereal2nd homeassistant/components/velux/* @Julius2342 homeassistant/components/version/* @fabaff homeassistant/components/vesync/* @markperdue @webdjoe diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 4729e11ff62..b071b354d74 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -7,5 +7,5 @@ ], "config_flow": true, "dependencies": [], - "codeowners": ["@ceral2nd"] + "codeowners": ["@cereal2nd"] } From 944cd7075319317ea638ea931876cc8204bafce7 Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 2 Aug 2019 10:00:33 +0200 Subject: [PATCH 025/273] Feature zwave preset modes (#25537) * Initial commit * Add some more code * Local tests passing * Remove unnecessary line * Add preset attributes to __init__ * Remove some more debugger lines * Add some tests * Fix comparision to None * Improve test coverage * Use unknown modes as presets * Bugfixes and test improvements * Add tests for unknown preset modes * linting * Improve mappings * Move PRESET_MANUFACTURER_SPECIFIC to zwave * Replace isinstance with cast * Add test for hvac_action * hvac_mode is never None * Improved mapping of current mode to hvac/preset modes * Fix bugs where hvac_mode is None * Add default hvac mode * Fixed default hvac mode * Fix linting * Make flake happy * Another linting * Make black happy * Complete list of default hvac modes * Add mapping to heat/cool eco * Fixed another bug where mapping goes wrong --- homeassistant/components/zwave/climate.py | 203 ++++++++++++--- tests/components/zwave/test_climate.py | 294 ++++++++++++++++++++-- 2 files changed, 440 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index 15a0c5ab78b..6f66c6f36c4 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -10,15 +10,19 @@ from homeassistant.components.climate.const import ( CURRENT_HVAC_IDLE, CURRENT_HVAC_OFF, DOMAIN, + HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, + PRESET_BOOST, + PRESET_NONE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_PRESET_MODE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback @@ -37,38 +41,57 @@ REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120) ATTR_OPERATING_STATE = "operating_state" ATTR_FAN_STATE = "fan_state" + +# Device is in manufacturer specific mode (e.g. setting the valve manually) +PRESET_MANUFACTURER_SPECIFIC = "Manufacturer Specific" + WORKAROUND_ZXT_120 = "zxt_120" DEVICE_MAPPINGS = {REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120} HVAC_STATE_MAPPINGS = { - "Off": HVAC_MODE_OFF, - "Heat": HVAC_MODE_HEAT, - "Heat Mode": HVAC_MODE_HEAT, - "Heat (Default)": HVAC_MODE_HEAT, - "Aux Heat": HVAC_MODE_HEAT, - "Furnace": HVAC_MODE_HEAT, - "Fan Only": HVAC_MODE_FAN_ONLY, - "Dry Air": HVAC_MODE_DRY, - "Moist Air": HVAC_MODE_DRY, - "Cool": HVAC_MODE_COOL, - "Auto": HVAC_MODE_HEAT_COOL, + "off": HVAC_MODE_OFF, + "heat": HVAC_MODE_HEAT, + "heat mode": HVAC_MODE_HEAT, + "heat (default)": HVAC_MODE_HEAT, + "aux heat": HVAC_MODE_HEAT, + "furnace": HVAC_MODE_HEAT, + "fan only": HVAC_MODE_FAN_ONLY, + "dry air": HVAC_MODE_DRY, + "moist air": HVAC_MODE_DRY, + "cool": HVAC_MODE_COOL, + "heat_cool": HVAC_MODE_HEAT_COOL, + "auto": HVAC_MODE_HEAT_COOL, } - HVAC_CURRENT_MAPPINGS = { - "Idle": CURRENT_HVAC_IDLE, - "Heat": CURRENT_HVAC_HEAT, - "Pending Heat": CURRENT_HVAC_IDLE, - "Heating": CURRENT_HVAC_HEAT, - "Cool": CURRENT_HVAC_COOL, - "Pending Cool": CURRENT_HVAC_IDLE, - "Cooling": CURRENT_HVAC_COOL, - "Fan Only": CURRENT_HVAC_FAN, - "Vent / Economiser": CURRENT_HVAC_FAN, - "Off": CURRENT_HVAC_OFF, + "idle": CURRENT_HVAC_IDLE, + "heat": CURRENT_HVAC_HEAT, + "pending heat": CURRENT_HVAC_IDLE, + "heating": CURRENT_HVAC_HEAT, + "cool": CURRENT_HVAC_COOL, + "pending cool": CURRENT_HVAC_IDLE, + "cooling": CURRENT_HVAC_COOL, + "fan only": CURRENT_HVAC_FAN, + "vent / economiser": CURRENT_HVAC_FAN, + "off": CURRENT_HVAC_OFF, } +PRESET_MAPPINGS = { + "full power": PRESET_BOOST, + "manufacturer specific": PRESET_MANUFACTURER_SPECIFIC, +} + +DEFAULT_HVAC_MODES = [ + HVAC_MODE_HEAT_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_DRY, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, +] + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old method of setting up Z-Wave climate devices.""" @@ -101,9 +124,13 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): self._target_temperature = None self._current_temperature = None self._hvac_action = None - self._hvac_list = None - self._hvac_mapping = None - self._hvac_mode = None + self._hvac_list = None # [zwave_mode] + self._hvac_mapping = None # {ha_mode:zwave_mode} + self._hvac_mode = None # ha_mode + self._default_hvac_mode = None # ha_mode + self._preset_mapping = None # {ha_mode:zwave_mode} + self._preset_list = None # [zwave_mode] + self._preset_mode = None # ha_mode if exists, else zwave_mode self._current_fan_mode = None self._fan_modes = None self._fan_state = None @@ -132,6 +159,8 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): support |= SUPPORT_FAN_MODE if self._zxt_120 == 1 and self.values.zxt_120_swing_mode: support |= SUPPORT_SWING_MODE + if self._preset_list: + support |= SUPPORT_PRESET_MODE return support def update_properties(self): @@ -140,26 +169,86 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): if self.values.mode: self._hvac_list = [] self._hvac_mapping = {} - hvac_list = self.values.mode.data_items - if hvac_list: - for mode in hvac_list: - ha_mode = HVAC_STATE_MAPPINGS.get(mode) + self._preset_list = [] + self._preset_mapping = {} + + mode_list = self.values.mode.data_items + if mode_list: + for mode in mode_list: + ha_mode = HVAC_STATE_MAPPINGS.get(str(mode).lower()) + ha_preset = PRESET_MAPPINGS.get(str(mode).lower()) if ha_mode and ha_mode not in self._hvac_mapping: self._hvac_mapping[ha_mode] = mode self._hvac_list.append(ha_mode) - continue - self._hvac_list.append(mode) + elif ha_preset and ha_preset not in self._preset_mapping: + self._preset_mapping[ha_preset] = mode + self._preset_list.append(ha_preset) + else: + # If nothing matches + self._preset_list.append(mode) + + # Default operation mode + for mode in DEFAULT_HVAC_MODES: + if mode in self._hvac_mapping.keys(): + self._default_hvac_mode = mode + break + + if self._preset_list: + # Presets are supported + self._preset_list.append(PRESET_NONE) + current_mode = self.values.mode.data - self._hvac_mode = next( + _LOGGER.debug("current_mode=%s", current_mode) + _hvac_temp = next( ( key for key, value in self._hvac_mapping.items() if value == current_mode ), - current_mode, + None, ) + + if _hvac_temp is None: + # The current mode is not a hvac mode + if ( + "heat" in current_mode.lower() + and HVAC_MODE_HEAT in self._hvac_mapping.keys() + ): + # The current preset modes maps to HVAC_MODE_HEAT + _LOGGER.debug("Mapped to HEAT") + self._hvac_mode = HVAC_MODE_HEAT + elif ( + "cool" in current_mode.lower() + and HVAC_MODE_COOL in self._hvac_mapping.keys() + ): + # The current preset modes maps to HVAC_MODE_COOL + _LOGGER.debug("Mapped to COOL") + self._hvac_mode = HVAC_MODE_COOL + else: + # The current preset modes maps to self._default_hvac_mode + _LOGGER.debug("Mapped to DEFAULT") + self._hvac_mode = self._default_hvac_mode + self._preset_mode = next( + ( + key + for key, value in self._preset_mapping.items() + if value == current_mode + ), + current_mode, + ) + else: + # The current mode is a hvac mode + self._hvac_mode = _hvac_temp + self._preset_mode = PRESET_NONE + + _LOGGER.debug("self._hvac_mapping=%s", self._hvac_mapping) _LOGGER.debug("self._hvac_list=%s", self._hvac_list) + _LOGGER.debug("self._hvac_mode=%s", self._hvac_mode) + _LOGGER.debug("self._default_hvac_mode=%s", self._default_hvac_mode) _LOGGER.debug("self._hvac_action=%s", self._hvac_action) + _LOGGER.debug("self._preset_mapping=%s", self._preset_mapping) + _LOGGER.debug("self._preset_list=%s", self._preset_list) + _LOGGER.debug("self._preset_mode=%s", self._preset_mode) # Current Temp if self.values.temperature: @@ -199,7 +288,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): # Operating state if self.values.operating_state: mode = self.values.operating_state.data - self._hvac_action = HVAC_CURRENT_MAPPINGS.get(mode) + self._hvac_action = HVAC_CURRENT_MAPPINGS.get(str(mode).lower(), mode) # Fan operating state if self.values.fan_state: @@ -247,7 +336,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): """ if self.values.mode: return self._hvac_mode - return HVAC_MODE_HEAT + return self._default_hvac_mode @property def hvac_modes(self): @@ -267,6 +356,26 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): """ return self._hvac_action + @property + def preset_mode(self): + """Return preset operation ie. eco, away. + + Need to be one of PRESET_*. + """ + if self.values.mode: + return self._preset_mode + return PRESET_NONE + + @property + def preset_modes(self): + """Return the list of available preset operation modes. + + Need to be a subset of PRESET_MODES. + """ + if self.values.mode: + return self._preset_list + return [] + @property def target_temperature(self): """Return the temperature we try to reach.""" @@ -274,24 +383,46 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): def set_temperature(self, **kwargs): """Set new target temperature.""" + _LOGGER.debug("Set temperature to %s", kwargs.get(ATTR_TEMPERATURE)) if kwargs.get(ATTR_TEMPERATURE) is None: return self.values.primary.data = kwargs.get(ATTR_TEMPERATURE) def set_fan_mode(self, fan_mode): """Set new target fan mode.""" + _LOGGER.debug("Set fan mode to %s", fan_mode) if not self.values.fan_mode: return self.values.fan_mode.data = fan_mode def set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" + _LOGGER.debug("Set hvac_mode to %s", hvac_mode) if not self.values.mode: return - self.values.mode.data = self._hvac_mapping.get(hvac_mode, hvac_mode) + operation_mode = self._hvac_mapping.get(hvac_mode) + _LOGGER.debug("Set operation_mode to %s", operation_mode) + self.values.mode.data = operation_mode + + def set_preset_mode(self, preset_mode): + """Set new target preset mode.""" + _LOGGER.debug("Set preset_mode to %s", preset_mode) + if not self.values.mode: + return + if preset_mode == PRESET_NONE: + # Activate the current hvac mode + self.update_properties() + operation_mode = self._hvac_mapping.get(self.hvac_mode) + _LOGGER.debug("Set operation_mode to %s", operation_mode) + self.values.mode.data = operation_mode + else: + operation_mode = self._preset_mapping.get(preset_mode, preset_mode) + _LOGGER.debug("Set operation_mode to %s", operation_mode) + self.values.mode.data = operation_mode def set_swing_mode(self, swing_mode): """Set new target swing mode.""" + _LOGGER.debug("Set swing_mode to %s", swing_mode) if self._zxt_120 == 1: if self.values.zxt_120_swing_mode: self.values.zxt_120_swing_mode.data = swing_mode diff --git a/tests/components/zwave/test_climate.py b/tests/components/zwave/test_climate.py index 430b901efbc..60a9dcd0dab 100644 --- a/tests/components/zwave/test_climate.py +++ b/tests/components/zwave/test_climate.py @@ -2,11 +2,23 @@ import pytest from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_COOL, + HVAC_MODES, HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + PRESET_BOOST, + PRESET_ECO, + PRESET_NONE, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_SWING_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.components.zwave import climate +from homeassistant.components.zwave.climate import DEFAULT_HVAC_MODES from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed @@ -19,9 +31,18 @@ def device(hass, mock_openzwave): values = MockEntityValues( primary=MockValue(data=1, node=node), temperature=MockValue(data=5, node=node, units=None), - mode=MockValue(data="test1", data_items=[0, 1, 2], node=node), + mode=MockValue( + data=HVAC_MODE_HEAT, + data_items=[ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + ], + node=node, + ), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), - operating_state=MockValue(data=6, node=node), + operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), fan_state=MockValue(data=7, node=node), ) device = climate.get_device(hass, node=node, values=values, node_config={}) @@ -37,9 +58,18 @@ def device_zxt_120(hass, mock_openzwave): values = MockEntityValues( primary=MockValue(data=1, node=node), temperature=MockValue(data=5, node=node, units=None), - mode=MockValue(data="test1", data_items=[0, 1, 2], node=node), + mode=MockValue( + data=HVAC_MODE_HEAT, + data_items=[ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + ], + node=node, + ), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), - operating_state=MockValue(data=6, node=node), + operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), fan_state=MockValue(data=7, node=node), zxt_120_swing_mode=MockValue(data="test3", data_items=[6, 7, 8], node=node), ) @@ -55,9 +85,13 @@ def device_mapping(hass, mock_openzwave): values = MockEntityValues( primary=MockValue(data=1, node=node), temperature=MockValue(data=5, node=node, units=None), - mode=MockValue(data="Off", data_items=["Off", "Cool", "Heat"], node=node), + mode=MockValue( + data="Heat", + data_items=["Off", "Cool", "Heat", "Full Power", "heat_cool"], + node=node, + ), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), - operating_state=MockValue(data=6, node=node), + operating_state=MockValue(data="heating", node=node), fan_state=MockValue(data=7, node=node), ) device = climate.get_device(hass, node=node, values=values, node_config={}) @@ -65,6 +99,83 @@ def device_mapping(hass, mock_openzwave): yield device +@pytest.fixture +def device_unknown(hass, mock_openzwave): + """Fixture to provide a precreated climate device. Test state unknown.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue(data=1, node=node), + temperature=MockValue(data=5, node=node, units=None), + mode=MockValue( + data="Heat", + data_items=["Off", "Cool", "Heat", "heat_cool", "Abcdefg"], + node=node, + ), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data="test4", node=node), + fan_state=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_heat_cool(hass, mock_openzwave): + """Fixture to provide a precreated climate device. Test state heat only.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue(data=1, node=node), + temperature=MockValue(data=5, node=node, units=None), + mode=MockValue( + data=HVAC_MODE_HEAT, + data_items=[ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + "Heat Eco", + "Cool Eco", + ], + node=node, + ), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data="test4", node=node), + fan_state=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +def test_default_hvac_modes(): + """Test wether all hvac modes are included in default_hvac_modes.""" + for hvac_mode in HVAC_MODES: + assert hvac_mode in DEFAULT_HVAC_MODES + + +def test_supported_features(device): + """Test supported features flags.""" + assert device.supported_features == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + + +def test_supported_features_preset_mode(device_mapping): + """Test supported features flags with swing mode.""" + device = device_mapping + assert ( + device.supported_features + == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + SUPPORT_PRESET_MODE + ) + + +def test_supported_features_swing_mode(device_zxt_120): + """Test supported features flags with swing mode.""" + device = device_zxt_120 + assert ( + device.supported_features + == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + SUPPORT_SWING_MODE + ) + + def test_zxt_120_swing_mode(device_zxt_120): """Test operation of the zxt 120 swing mode.""" device = device_zxt_120 @@ -107,7 +218,24 @@ def test_default_target_temperature(device): def test_data_lists(device): """Test data lists from zwave value items.""" assert device.fan_modes == [3, 4, 5] - assert device.hvac_modes == [0, 1, 2] + assert device.hvac_modes == [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + ] + assert device.preset_modes == [] + device.values.mode = None + assert device.preset_modes == [] + + +def test_data_lists_mapping(device_mapping): + """Test data lists from zwave value items.""" + device = device_mapping + assert device.hvac_modes == ["off", "cool", "heat", "heat_cool"] + assert device.preset_modes == ["boost", "none"] + device.values.mode = None + assert device.preset_modes == [] def test_target_value_set(device): @@ -121,21 +249,56 @@ def test_target_value_set(device): def test_operation_value_set(device): """Test values changed for climate device.""" - assert device.values.mode.data == "test1" - device.set_hvac_mode("test_set") - assert device.values.mode.data == "test_set" + assert device.values.mode.data == HVAC_MODE_HEAT + device.set_hvac_mode(HVAC_MODE_COOL) + assert device.values.mode.data == HVAC_MODE_COOL + device.set_preset_mode(PRESET_ECO) + assert device.values.mode.data == PRESET_ECO + device.set_preset_mode(PRESET_NONE) + assert device.values.mode.data == HVAC_MODE_HEAT_COOL + device.values.mode = None + device.set_hvac_mode("test_set_failes") + assert device.values.mode is None + device.set_preset_mode("test_set_failes") + assert device.values.mode is None def test_operation_value_set_mapping(device_mapping): """Test values changed for climate device. Mapping.""" device = device_mapping - assert device.values.mode.data == "Off" - device.set_hvac_mode(HVAC_MODE_HEAT) assert device.values.mode.data == "Heat" device.set_hvac_mode(HVAC_MODE_COOL) assert device.values.mode.data == "Cool" device.set_hvac_mode(HVAC_MODE_OFF) assert device.values.mode.data == "Off" + device.set_preset_mode(PRESET_BOOST) + assert device.values.mode.data == "Full Power" + device.set_preset_mode(PRESET_ECO) + assert device.values.mode.data == "eco" + + +def test_operation_value_set_unknown(device_unknown): + """Test values changed for climate device. Unknown.""" + device = device_unknown + assert device.values.mode.data == "Heat" + device.set_preset_mode("Abcdefg") + assert device.values.mode.data == "Abcdefg" + device.set_preset_mode(PRESET_NONE) + assert device.values.mode.data == HVAC_MODE_HEAT_COOL + + +def test_operation_value_set_heat_cool(device_heat_cool): + """Test values changed for climate device. Heat/Cool only.""" + device = device_heat_cool + assert device.values.mode.data == HVAC_MODE_HEAT + device.set_preset_mode("Heat Eco") + assert device.values.mode.data == "Heat Eco" + device.set_preset_mode(PRESET_NONE) + assert device.values.mode.data == HVAC_MODE_HEAT + device.set_preset_mode("Cool Eco") + assert device.values.mode.data == "Cool Eco" + device.set_preset_mode(PRESET_NONE) + assert device.values.mode.data == HVAC_MODE_COOL def test_fan_mode_value_set(device): @@ -143,6 +306,9 @@ def test_fan_mode_value_set(device): assert device.values.fan_mode.data == "test2" device.set_fan_mode("test_fan_set") assert device.values.fan_mode.data == "test_fan_set" + device.values.fan_mode = None + device.set_fan_mode("test_fan_set_failes") + assert device.values.fan_mode is None def test_target_value_changed(device): @@ -163,25 +329,85 @@ def test_temperature_value_changed(device): def test_operation_value_changed(device): """Test values changed for climate device.""" - assert device.hvac_mode == "test1" - device.values.mode.data = "test_updated" + assert device.hvac_mode == HVAC_MODE_HEAT + assert device.preset_mode == PRESET_NONE + device.values.mode.data = HVAC_MODE_COOL value_changed(device.values.mode) - assert device.hvac_mode == "test_updated" + assert device.hvac_mode == HVAC_MODE_COOL + assert device.preset_mode == PRESET_NONE + device.values.mode.data = HVAC_MODE_OFF + value_changed(device.values.mode) + assert device.hvac_mode == HVAC_MODE_OFF + assert device.preset_mode == PRESET_NONE + device.values.mode = None + assert device.hvac_mode == HVAC_MODE_HEAT_COOL + assert device.preset_mode == PRESET_NONE + + +def test_operation_value_changed_preset(device_mapping): + """Test preset changed for climate device.""" + device = device_mapping + assert device.hvac_mode == HVAC_MODE_HEAT + assert device.preset_mode == PRESET_NONE + device.values.mode.data = PRESET_ECO + value_changed(device.values.mode) + assert device.hvac_mode == HVAC_MODE_HEAT_COOL + assert device.preset_mode == PRESET_ECO def test_operation_value_changed_mapping(device_mapping): """Test values changed for climate device. Mapping.""" device = device_mapping - assert device.hvac_mode == "off" - device.values.mode.data = "Heat" - value_changed(device.values.mode) assert device.hvac_mode == HVAC_MODE_HEAT - device.values.mode.data = "Cool" - value_changed(device.values.mode) - assert device.hvac_mode == HVAC_MODE_COOL + assert device.preset_mode == PRESET_NONE device.values.mode.data = "Off" value_changed(device.values.mode) assert device.hvac_mode == HVAC_MODE_OFF + assert device.preset_mode == PRESET_NONE + device.values.mode.data = "Cool" + value_changed(device.values.mode) + assert device.hvac_mode == HVAC_MODE_COOL + assert device.preset_mode == PRESET_NONE + + +def test_operation_value_changed_mapping_preset(device_mapping): + """Test values changed for climate device. Mapping with presets.""" + device = device_mapping + assert device.hvac_mode == HVAC_MODE_HEAT + assert device.preset_mode == PRESET_NONE + device.values.mode.data = "Full Power" + value_changed(device.values.mode) + assert device.hvac_mode == HVAC_MODE_HEAT_COOL + assert device.preset_mode == PRESET_BOOST + device.values.mode = None + assert device.hvac_mode == HVAC_MODE_HEAT_COOL + assert device.preset_mode == PRESET_NONE + + +def test_operation_value_changed_unknown(device_unknown): + """Test preset changed for climate device. Unknown.""" + device = device_unknown + assert device.hvac_mode == HVAC_MODE_HEAT + assert device.preset_mode == PRESET_NONE + device.values.mode.data = "Abcdefg" + value_changed(device.values.mode) + assert device.hvac_mode == HVAC_MODE_HEAT_COOL + assert device.preset_mode == "Abcdefg" + + +def test_operation_value_changed_heat_cool(device_heat_cool): + """Test preset changed for climate device. Heat/Cool only.""" + device = device_heat_cool + assert device.hvac_mode == HVAC_MODE_HEAT + assert device.preset_mode == PRESET_NONE + device.values.mode.data = "Cool Eco" + value_changed(device.values.mode) + assert device.hvac_mode == HVAC_MODE_COOL + assert device.preset_mode == "Cool Eco" + device.values.mode.data = "Heat Eco" + value_changed(device.values.mode) + assert device.hvac_mode == HVAC_MODE_HEAT + assert device.preset_mode == "Heat Eco" def test_fan_mode_value_changed(device): @@ -190,3 +416,29 @@ def test_fan_mode_value_changed(device): device.values.fan_mode.data = "test_updated_fan" value_changed(device.values.fan_mode) assert device.fan_mode == "test_updated_fan" + + +def test_hvac_action_value_changed(device): + """Test values changed for climate device.""" + assert device.hvac_action == CURRENT_HVAC_HEAT + device.values.operating_state.data = CURRENT_HVAC_COOL + value_changed(device.values.operating_state) + assert device.hvac_action == CURRENT_HVAC_COOL + + +def test_hvac_action_value_changed_mapping(device_mapping): + """Test values changed for climate device.""" + device = device_mapping + assert device.hvac_action == CURRENT_HVAC_HEAT + device.values.operating_state.data = "cooling" + value_changed(device.values.operating_state) + assert device.hvac_action == CURRENT_HVAC_COOL + + +def test_hvac_action_value_changed_unknown(device_unknown): + """Test values changed for climate device.""" + device = device_unknown + assert device.hvac_action == "test4" + device.values.operating_state.data = "another_hvac_action" + value_changed(device.values.operating_state) + assert device.hvac_action == "another_hvac_action" From 39257164a92e13094ec37da0e1e1b6c50d1bd4c9 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 2 Aug 2019 10:13:00 +0200 Subject: [PATCH 026/273] UniFi - allow configuration to not track clients or devices (#25642) * Allow configuration to not track clients or devices --- homeassistant/components/unifi/__init__.py | 4 ++ homeassistant/components/unifi/const.py | 2 + .../components/unifi/device_tracker.py | 72 ++++++++++--------- tests/components/unifi/test_device_tracker.py | 39 +++++++++- 4 files changed, 84 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 726d4793085..f4df139001d 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -11,6 +11,8 @@ from .const import ( CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, + CONF_DONT_TRACK_CLIENTS, + CONF_DONT_TRACK_DEVICES, CONF_SITE_ID, CONF_SSID_FILTER, CONTROLLER_ID, @@ -28,6 +30,8 @@ CONTROLLER_SCHEMA = vol.Schema( vol.Optional(CONF_BLOCK_CLIENT, default=[]): vol.All( cv.ensure_list, [cv.string] ), + vol.Optional(CONF_DONT_TRACK_CLIENTS): cv.boolean, + vol.Optional(CONF_DONT_TRACK_DEVICES): cv.boolean, vol.Optional(CONF_DETECTION_TIME): vol.All( cv.time_period, cv.positive_timedelta ), diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index 383b018264a..1295849704c 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -13,6 +13,8 @@ UNIFI_CONFIG = "unifi_config" CONF_BLOCK_CLIENT = "block_client" CONF_DETECTION_TIME = "detection_time" +CONF_DONT_TRACK_CLIENTS = "dont_track_clients" +CONF_DONT_TRACK_DEVICES = "dont_track_devices" CONF_SSID_FILTER = "ssid_filter" ATTR_MANUFACTURER = "Ubiquiti Networks" diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 4046f5f63d2..ce5a1a7f608 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -28,6 +28,8 @@ from .const import ( ATTR_MANUFACTURER, CONF_CONTROLLER, CONF_DETECTION_TIME, + CONF_DONT_TRACK_CLIENTS, + CONF_DONT_TRACK_DEVICES, CONF_SITE_ID, CONF_SSID_FILTER, CONTROLLER_ID, @@ -154,46 +156,52 @@ def update_items(controller, async_add_entities, tracked): """Update tracked device state from the controller.""" new_tracked = [] - for client_id in controller.api.clients: + if not controller.unifi_config.get(CONF_DONT_TRACK_CLIENTS, False): - if client_id in tracked: + for client_id in controller.api.clients: + + if client_id in tracked: + LOGGER.debug( + "Updating UniFi tracked client %s (%s)", + tracked[client_id].entity_id, + tracked[client_id].client.mac, + ) + tracked[client_id].async_schedule_update_ha_state() + continue + + client = controller.api.clients[client_id] + + if ( + not client.is_wired + and CONF_SSID_FILTER in controller.unifi_config + and client.essid not in controller.unifi_config[CONF_SSID_FILTER] + ): + continue + + tracked[client_id] = UniFiClientTracker(client, controller) + new_tracked.append(tracked[client_id]) LOGGER.debug( - "Updating UniFi tracked client %s (%s)", - tracked[client_id].entity_id, - tracked[client_id].client.mac, + "New UniFi client tracker %s (%s)", client.hostname, client.mac ) - tracked[client_id].async_schedule_update_ha_state() - continue - client = controller.api.clients[client_id] + if not controller.unifi_config.get(CONF_DONT_TRACK_DEVICES, False): - if ( - not client.is_wired - and CONF_SSID_FILTER in controller.unifi_config - and client.essid not in controller.unifi_config[CONF_SSID_FILTER] - ): - continue + for device_id in controller.api.devices: - tracked[client_id] = UniFiClientTracker(client, controller) - new_tracked.append(tracked[client_id]) - LOGGER.debug("New UniFi client tracker %s (%s)", client.hostname, client.mac) + if device_id in tracked: + LOGGER.debug( + "Updating UniFi tracked device %s (%s)", + tracked[device_id].entity_id, + tracked[device_id].device.mac, + ) + tracked[device_id].async_schedule_update_ha_state() + continue - for device_id in controller.api.devices: + device = controller.api.devices[device_id] - if device_id in tracked: - LOGGER.debug( - "Updating UniFi tracked device %s (%s)", - tracked[device_id].entity_id, - tracked[device_id].device.mac, - ) - tracked[device_id].async_schedule_update_ha_state() - continue - - device = controller.api.devices[device_id] - - tracked[device_id] = UniFiDeviceTracker(device, controller) - new_tracked.append(tracked[device_id]) - LOGGER.debug("New UniFi device tracker %s (%s)", device.name, device.mac) + tracked[device_id] = UniFiDeviceTracker(device, controller) + new_tracked.append(tracked[device_id]) + LOGGER.debug("New UniFi device tracker %s (%s)", device.name, device.mac) if new_tracked: async_add_entities(new_tracked) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 5accbb584b4..fb13bef42aa 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -22,6 +22,7 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, CONF_VERIFY_SSL, + STATE_UNAVAILABLE, ) from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component @@ -211,7 +212,7 @@ async def test_tracked_devices(hass, mock_controller): await hass.async_block_till_done() device_1 = hass.states.get("device_tracker.device_1") - assert device_1.state == "unavailable" + assert device_1.state == STATE_UNAVAILABLE async def test_restoring_client(hass, mock_controller): @@ -243,3 +244,39 @@ async def test_restoring_client(hass, mock_controller): device_1 = hass.states.get("device_tracker.client_1") assert device_1 is not None + + +async def test_dont_track_clients(hass, mock_controller): + """Test dont track clients config works.""" + mock_controller.mock_client_responses.append([CLIENT_1]) + mock_controller.mock_device_responses.append([DEVICE_1]) + mock_controller.unifi_config = {unifi.CONF_DONT_TRACK_CLIENTS: True} + + await setup_controller(hass, mock_controller) + assert len(mock_controller.mock_requests) == 2 + assert len(hass.states.async_all()) == 3 + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is None + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 is not None + assert device_1.state == "not_home" + + +async def test_dont_track_devices(hass, mock_controller): + """Test dont track devices config works.""" + mock_controller.mock_client_responses.append([CLIENT_1]) + mock_controller.mock_device_responses.append([DEVICE_1]) + mock_controller.unifi_config = {unifi.CONF_DONT_TRACK_DEVICES: True} + + await setup_controller(hass, mock_controller) + assert len(mock_controller.mock_requests) == 2 + assert len(hass.states.async_all()) == 3 + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is not None + assert client_1.state == "not_home" + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 is None From 77e4ff94fd8dd8ae8bb7f715f67fad43eaa189a5 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 2 Aug 2019 06:05:23 -0400 Subject: [PATCH 027/273] ZHA code cleanup. (#25644) * isort ZHA imports. * Sort zha channel registry. * Sort ZHA core registry. * Sort ZHA core consts. --- homeassistant/components/zha/__init__.py | 6 +- homeassistant/components/zha/api.py | 47 +-- homeassistant/components/zha/binary_sensor.py | 49 +-- .../components/zha/core/channels/__init__.py | 29 +- .../components/zha/core/channels/closures.py | 2 + .../components/zha/core/channels/general.py | 4 +- .../zha/core/channels/homeautomation.py | 6 +- .../components/zha/core/channels/hvac.py | 2 + .../components/zha/core/channels/lighting.py | 1 + .../components/zha/core/channels/registry.py | 49 ++- .../components/zha/core/channels/security.py | 4 +- homeassistant/components/zha/core/const.py | 216 ++++++------- homeassistant/components/zha/core/device.py | 104 +++--- .../components/zha/core/discovery.py | 19 +- homeassistant/components/zha/core/gateway.py | 120 ++++--- homeassistant/components/zha/core/helpers.py | 26 +- .../components/zha/core/registries.py | 303 +++++++++--------- homeassistant/components/zha/core/store.py | 7 +- .../components/zha/device_tracker.py | 10 +- homeassistant/components/zha/entity.py | 8 +- homeassistant/components/zha/fan.py | 9 +- homeassistant/components/zha/light.py | 16 +- homeassistant/components/zha/lock.py | 12 +- homeassistant/components/zha/sensor.py | 95 +++--- homeassistant/components/zha/switch.py | 8 +- tests/components/zha/test_api.py | 38 +-- 26 files changed, 627 insertions(+), 563 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 5905f86d1bd..b0d1ec42ec7 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -26,7 +26,7 @@ from .core.const import ( DEFAULT_BAUDRATE, DEFAULT_RADIO_TYPE, DOMAIN, - ENABLE_QUIRKS, + CONF_ENABLE_QUIRKS, RadioType, ) from .core.registries import establish_device_mappings @@ -46,7 +46,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_DEVICE_CONFIG, default={}): vol.Schema( {cv.string: DEVICE_CONFIG_SCHEMA_ENTRY} ), - vol.Optional(ENABLE_QUIRKS, default=True): cv.boolean, + vol.Optional(CONF_ENABLE_QUIRKS, default=True): cv.boolean, } ) }, @@ -99,7 +99,7 @@ async def async_setup_entry(hass, config_entry): hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS] = [] config = hass.data[DATA_ZHA].get(DATA_ZHA_CONFIG, {}) - if config.get(ENABLE_QUIRKS, True): + if config.get(CONF_ENABLE_QUIRKS, True): # needs to be done here so that the ZHA module is finished loading # before zhaquirks is imported # pylint: disable=W0611, W0612 diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 0e26bfab592..9b482dfb4f5 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -21,16 +21,16 @@ from .core.const import ( ATTR_ENDPOINT_ID, ATTR_MANUFACTURER, ATTR_VALUE, - CLIENT_COMMANDS, + CLUSTER_COMMANDS_CLIENT, DATA_ZHA, DATA_ZHA_GATEWAY, DOMAIN, - IN, + CLUSTER_TYPE_IN, MFG_CLUSTER_ID_START, - NAME, - OUT, - SERVER, - SERVER_COMMANDS, + ATTR_NAME, + CLUSTER_TYPE_OUT, + CLUSTER_COMMAND_SERVER, + CLUSTER_COMMANDS_SERVER, ) from .core.helpers import async_is_bindable_target, convert_ieee, get_matched_clusters @@ -74,7 +74,7 @@ SERVICE_SCHEMAS = { vol.Required(ATTR_IEEE): convert_ieee, vol.Required(ATTR_ENDPOINT_ID): cv.positive_int, vol.Required(ATTR_CLUSTER_ID): cv.positive_int, - vol.Optional(ATTR_CLUSTER_TYPE, default=IN): cv.string, + vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, vol.Required(ATTR_ATTRIBUTE): cv.positive_int, vol.Required(ATTR_VALUE): cv.string, vol.Optional(ATTR_MANUFACTURER): cv.positive_int, @@ -85,7 +85,7 @@ SERVICE_SCHEMAS = { vol.Required(ATTR_IEEE): convert_ieee, vol.Required(ATTR_ENDPOINT_ID): cv.positive_int, vol.Required(ATTR_CLUSTER_ID): cv.positive_int, - vol.Optional(ATTR_CLUSTER_TYPE, default=IN): cv.string, + vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, vol.Required(ATTR_COMMAND): cv.positive_int, vol.Required(ATTR_COMMAND_TYPE): cv.string, vol.Optional(ATTR_ARGS, default=""): cv.string, @@ -155,7 +155,10 @@ def async_get_device_info(hass, device, ha_device_registry=None): ret_device = {} ret_device.update(device.device_info) ret_device["entities"] = [ - {"entity_id": entity_ref.reference_id, NAME: entity_ref.device_info[NAME]} + { + "entity_id": entity_ref.reference_id, + ATTR_NAME: entity_ref.device_info[ATTR_NAME], + } for entity_ref in zha_gateway.device_registry[device.ieee] ] @@ -201,21 +204,21 @@ async def websocket_device_clusters(hass, connection, msg): if zha_device is not None: clusters_by_endpoint = zha_device.async_get_clusters() for ep_id, clusters in clusters_by_endpoint.items(): - for c_id, cluster in clusters[IN].items(): + for c_id, cluster in clusters[CLUSTER_TYPE_IN].items(): response_clusters.append( { - TYPE: IN, + TYPE: CLUSTER_TYPE_IN, ID: c_id, - NAME: cluster.__class__.__name__, + ATTR_NAME: cluster.__class__.__name__, "endpoint_id": ep_id, } ) - for c_id, cluster in clusters[OUT].items(): + for c_id, cluster in clusters[CLUSTER_TYPE_OUT].items(): response_clusters.append( { - TYPE: OUT, + TYPE: CLUSTER_TYPE_OUT, ID: c_id, - NAME: cluster.__class__.__name__, + ATTR_NAME: cluster.__class__.__name__, "endpoint_id": ep_id, } ) @@ -250,7 +253,9 @@ async def websocket_device_cluster_attributes(hass, connection, msg): ) if attributes is not None: for attr_id in attributes: - cluster_attributes.append({ID: attr_id, NAME: attributes[attr_id][0]}) + cluster_attributes.append( + {ID: attr_id, ATTR_NAME: attributes[attr_id][0]} + ) _LOGGER.debug( "Requested attributes for: %s %s %s %s", "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), @@ -289,20 +294,20 @@ async def websocket_device_cluster_commands(hass, connection, msg): ) if commands is not None: - for cmd_id in commands[CLIENT_COMMANDS]: + for cmd_id in commands[CLUSTER_COMMANDS_CLIENT]: cluster_commands.append( { TYPE: CLIENT, ID: cmd_id, - NAME: commands[CLIENT_COMMANDS][cmd_id][0], + ATTR_NAME: commands[CLUSTER_COMMANDS_CLIENT][cmd_id][0], } ) - for cmd_id in commands[SERVER_COMMANDS]: + for cmd_id in commands[CLUSTER_COMMANDS_SERVER]: cluster_commands.append( { - TYPE: SERVER, + TYPE: CLUSTER_COMMAND_SERVER, ID: cmd_id, - NAME: commands[SERVER_COMMANDS][cmd_id][0], + ATTR_NAME: commands[CLUSTER_COMMANDS_SERVER][cmd_id][0], } ) _LOGGER.debug( diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 7ca7dbf9db3..082cd4542e7 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -2,34 +2,35 @@ import logging from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_GAS, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_VIBRATION, DOMAIN, BinarySensorDevice, - DEVICE_CLASS_MOVING, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_SMOKE, - DEVICE_CLASS_GAS, - DEVICE_CLASS_VIBRATION, - DEVICE_CLASS_OCCUPANCY, ) from homeassistant.const import STATE_ON from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .core.const import ( + SENSOR_ACCELERATION, + CHANNEL_ATTRIBUTE, DATA_ZHA, DATA_ZHA_DISPATCHERS, - ZHA_DISCOVERY_NEW, - ON_OFF_CHANNEL, - ZONE_CHANNEL, - SIGNAL_ATTR_UPDATED, - ATTRIBUTE_CHANNEL, - UNKNOWN, - OPENING, - ZONE, - OCCUPANCY, + SENSOR_OCCUPANCY, + CHANNEL_ON_OFF, + SENSOR_OPENING, SENSOR_TYPE, - ACCELERATION, + SIGNAL_ATTR_UPDATED, + UNKNOWN, + ZHA_DISCOVERY_NEW, + ZONE, + CHANNEL_ZONE, ) from .entity import ZhaEntity @@ -54,10 +55,10 @@ async def get_ias_device_class(channel): DEVICE_CLASS_REGISTRY = { UNKNOWN: None, - OPENING: DEVICE_CLASS_OPENING, + SENSOR_OPENING: DEVICE_CLASS_OPENING, ZONE: get_ias_device_class, - OCCUPANCY: DEVICE_CLASS_OCCUPANCY, - ACCELERATION: DEVICE_CLASS_MOVING, + SENSOR_OCCUPANCY: DEVICE_CLASS_OCCUPANCY, + SENSOR_ACCELERATION: DEVICE_CLASS_MOVING, } @@ -108,9 +109,9 @@ class BinarySensor(ZhaEntity, BinarySensorDevice): """Initialize the ZHA binary sensor.""" super().__init__(**kwargs) self._device_state_attributes = {} - self._zone_channel = self.cluster_channels.get(ZONE_CHANNEL) - self._on_off_channel = self.cluster_channels.get(ON_OFF_CHANNEL) - self._attr_channel = self.cluster_channels.get(ATTRIBUTE_CHANNEL) + self._zone_channel = self.cluster_channels.get(CHANNEL_ZONE) + self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) + self._attr_channel = self.cluster_channels.get(CHANNEL_ATTRIBUTE) self._zha_sensor_type = kwargs[SENSOR_TYPE] async def _determine_device_class(self): diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 6183d36427c..98919e46555 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -13,20 +13,21 @@ from random import uniform from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from ..helpers import ( - configure_reporting, - construct_unique_id, - safe_read, - get_attr_id_by_name, - bind_cluster, - LogMixin, -) + from ..const import ( + CHANNEL_ATTRIBUTE, + CHANNEL_EVENT_RELAY, REPORT_CONFIG_DEFAULT, SIGNAL_ATTR_UPDATED, - ATTRIBUTE_CHANNEL, - EVENT_RELAY_CHANNEL, - ZDO_CHANNEL, + CHANNEL_ZDO, +) +from ..helpers import ( + LogMixin, + bind_cluster, + configure_reporting, + construct_unique_id, + get_attr_id_by_name, + safe_read, ) from ..registries import CLUSTER_REPORT_CONFIGS @@ -232,7 +233,7 @@ class ZigbeeChannel(LogMixin): class AttributeListeningChannel(ZigbeeChannel): """Channel for attribute reports from the cluster.""" - CHANNEL_NAME = ATTRIBUTE_CHANNEL + CHANNEL_NAME = CHANNEL_ATTRIBUTE def __init__(self, cluster, device): """Initialize AttributeListeningChannel.""" @@ -266,7 +267,7 @@ class ZDOChannel(LogMixin): def __init__(self, cluster, device): """Initialize ZDOChannel.""" - self.name = ZDO_CHANNEL + self.name = CHANNEL_ZDO self._cluster = cluster self._zha_device = device self._status = ChannelStatus.CREATED @@ -320,7 +321,7 @@ class ZDOChannel(LogMixin): class EventRelayChannel(ZigbeeChannel): """Event relay that can be attached to zigbee clusters.""" - CHANNEL_NAME = EVENT_RELAY_CHANNEL + CHANNEL_NAME = CHANNEL_EVENT_RELAY @callback def attribute_updated(self, attrid, value): diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index 5c3a3d46f0d..23d174c08b1 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -5,8 +5,10 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ import logging + from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send + from . import ZigbeeChannel from ..const import SIGNAL_ATTR_UPDATED diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 5d624c832dd..388524b62e4 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -5,12 +5,14 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ import logging + from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later + from . import ZigbeeChannel, parse_and_log_command -from ..helpers import get_attr_id_by_name from ..const import SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL +from ..helpers import get_attr_id_by_name _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index dee6e43f475..05b9b591cac 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -5,9 +5,11 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ import logging + from homeassistant.helpers.dispatcher import async_dispatcher_send + from . import AttributeListeningChannel -from ..const import SIGNAL_ATTR_UPDATED, ELECTRICAL_MEASUREMENT_CHANNEL +from ..const import CHANNEL_ELECTRICAL_MEASUREMENT, SIGNAL_ATTR_UPDATED _LOGGER = logging.getLogger(__name__) @@ -15,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) class ElectricalMeasurementChannel(AttributeListeningChannel): """Channel that polls active power level.""" - CHANNEL_NAME = ELECTRICAL_MEASUREMENT_CHANNEL + CHANNEL_NAME = CHANNEL_ELECTRICAL_MEASUREMENT async def async_update(self): """Retrieve latest state.""" diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 164497abd35..7e38af4a0d7 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -5,8 +5,10 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ import logging + from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send + from . import ZigbeeChannel from ..const import SIGNAL_ATTR_UPDATED diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index e36da629ace..05d88d82288 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ import logging + from . import ZigbeeChannel _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/channels/registry.py b/homeassistant/components/zha/core/channels/registry.py index 9a590600f70..86527e0ac4a 100644 --- a/homeassistant/components/zha/core/channels/registry.py +++ b/homeassistant/components/zha/core/channels/registry.py @@ -5,13 +5,12 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ from . import ZigbeeChannel - from .closures import DoorLockChannel from .general import ( - OnOffChannel, - LevelControlChannel, - PowerConfigurationChannel, BasicChannel, + LevelControlChannel, + OnOffChannel, + PowerConfigurationChannel, ) from .homeautomation import ElectricalMeasurementChannel from .hvac import FanChannel @@ -27,27 +26,27 @@ def populate_channel_registry(): ZIGBEE_CHANNEL_REGISTRY.update( { - zcl.clusters.general.Alarms.cluster_id: ZigbeeChannel, - zcl.clusters.general.Commissioning.cluster_id: ZigbeeChannel, - zcl.clusters.general.Identify.cluster_id: ZigbeeChannel, - zcl.clusters.general.Groups.cluster_id: ZigbeeChannel, - zcl.clusters.general.Scenes.cluster_id: ZigbeeChannel, - zcl.clusters.general.Partition.cluster_id: ZigbeeChannel, - zcl.clusters.general.Ota.cluster_id: ZigbeeChannel, - zcl.clusters.general.PowerProfile.cluster_id: ZigbeeChannel, - zcl.clusters.general.ApplianceControl.cluster_id: ZigbeeChannel, - zcl.clusters.general.PollControl.cluster_id: ZigbeeChannel, - zcl.clusters.general.GreenPowerProxy.cluster_id: ZigbeeChannel, - zcl.clusters.general.OnOffConfiguration.cluster_id: ZigbeeChannel, - zcl.clusters.general.OnOff.cluster_id: OnOffChannel, - zcl.clusters.general.LevelControl.cluster_id: LevelControlChannel, - zcl.clusters.lighting.Color.cluster_id: ColorChannel, - zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: ElectricalMeasurementChannel, - zcl.clusters.general.PowerConfiguration.cluster_id: PowerConfigurationChannel, - zcl.clusters.general.Basic.cluster_id: BasicChannel, - zcl.clusters.security.IasZone.cluster_id: IASZoneChannel, - zcl.clusters.hvac.Fan.cluster_id: FanChannel, - zcl.clusters.lightlink.LightLink.cluster_id: ZigbeeChannel, zcl.clusters.closures.DoorLock.cluster_id: DoorLockChannel, + zcl.clusters.general.Alarms.cluster_id: ZigbeeChannel, + zcl.clusters.general.ApplianceControl.cluster_id: ZigbeeChannel, + zcl.clusters.general.Basic.cluster_id: BasicChannel, + zcl.clusters.general.Commissioning.cluster_id: ZigbeeChannel, + zcl.clusters.general.GreenPowerProxy.cluster_id: ZigbeeChannel, + zcl.clusters.general.Groups.cluster_id: ZigbeeChannel, + zcl.clusters.general.Identify.cluster_id: ZigbeeChannel, + zcl.clusters.general.LevelControl.cluster_id: LevelControlChannel, + zcl.clusters.general.OnOff.cluster_id: OnOffChannel, + zcl.clusters.general.OnOffConfiguration.cluster_id: ZigbeeChannel, + zcl.clusters.general.Ota.cluster_id: ZigbeeChannel, + zcl.clusters.general.Partition.cluster_id: ZigbeeChannel, + zcl.clusters.general.PollControl.cluster_id: ZigbeeChannel, + zcl.clusters.general.PowerConfiguration.cluster_id: PowerConfigurationChannel, + zcl.clusters.general.PowerProfile.cluster_id: ZigbeeChannel, + zcl.clusters.general.Scenes.cluster_id: ZigbeeChannel, + zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: ElectricalMeasurementChannel, + zcl.clusters.hvac.Fan.cluster_id: FanChannel, + zcl.clusters.lighting.Color.cluster_id: ColorChannel, + zcl.clusters.lightlink.LightLink.cluster_id: ZigbeeChannel, + zcl.clusters.security.IasZone.cluster_id: IASZoneChannel, } ) diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 6b2a8af837f..20b5ce7ba8f 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -5,11 +5,13 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ import logging + from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send + from . import ZigbeeChannel -from ..helpers import bind_cluster from ..const import SIGNAL_ATTR_UPDATED +from ..helpers import bind_cluster _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 8f58f0c9af0..7a4f5f94897 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -10,132 +10,98 @@ from homeassistant.components.lock import DOMAIN as LOCK from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH -DOMAIN = "zha" +ATTR_ARGS = "args" +ATTR_ATTRIBUTE = "attribute" +ATTR_AVAILABLE = "available" +ATTR_CLUSTER_ID = "cluster_id" +ATTR_CLUSTER_TYPE = "cluster_type" +ATTR_COMMAND = "command" +ATTR_COMMAND_TYPE = "command_type" +ATTR_ENDPOINT_ID = "endpoint_id" +ATTR_IEEE = "ieee" +ATTR_LAST_SEEN = "last_seen" +ATTR_LEVEL = "level" +ATTR_LQI = "lqi" +ATTR_MANUFACTURER = "manufacturer" +ATTR_MANUFACTURER_CODE = "manufacturer_code" +ATTR_MODEL = "model" +ATTR_NAME = "name" +ATTR_NWK = "nwk" +ATTR_POWER_SOURCE = "power_source" +ATTR_QUIRK_APPLIED = "quirk_applied" +ATTR_QUIRK_CLASS = "quirk_class" +ATTR_RSSI = "rssi" +ATTR_SIGNATURE = "signature" +ATTR_TYPE = "type" +ATTR_VALUE = "value" BAUD_RATES = [2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000] -DATA_ZHA = "zha" -DATA_ZHA_CONFIG = "config" -DATA_ZHA_BRIDGE_ID = "zha_bridge_id" -DATA_ZHA_DISPATCHERS = "zha_dispatchers" -DATA_ZHA_CORE_EVENTS = "zha_core_events" -DATA_ZHA_GATEWAY = "zha_gateway" -ZHA_DISCOVERY_NEW = "zha_discovery_new_{}" +CHANNEL_ATTRIBUTE = "attribute" +CHANNEL_BASIC = "basic" +CHANNEL_COLOR = "light_color" +CHANNEL_DOORLOCK = "door_lock" +CHANNEL_ELECTRICAL_MEASUREMENT = "electrical_measurement" +CHANNEL_EVENT_RELAY = "event_relay" +CHANNEL_FAN = "fan" +CHANNEL_LEVEL = ATTR_LEVEL +CHANNEL_ON_OFF = "on_off" +CHANNEL_POWER_CONFIGURATION = "power" +CHANNEL_ZDO = "zdo" +CHANNEL_ZONE = ZONE = "ias_zone" + +CLUSTER_COMMAND_SERVER = "server" +CLUSTER_COMMANDS_CLIENT = "client_commands" +CLUSTER_COMMANDS_SERVER = "server_commands" +CLUSTER_TYPE_IN = "in" +CLUSTER_TYPE_OUT = "out" COMPONENTS = (BINARY_SENSOR, DEVICE_TRACKER, FAN, LIGHT, LOCK, SENSOR, SWITCH) CONF_BAUDRATE = "baudrate" CONF_DATABASE = "database_path" CONF_DEVICE_CONFIG = "device_config" +CONF_ENABLE_QUIRKS = "enable_quirks" CONF_RADIO_TYPE = "radio_type" CONF_USB_PATH = "usb_path" -DATA_DEVICE_CONFIG = "zha_device_config" -ENABLE_QUIRKS = "enable_quirks" - -RADIO = "radio" -RADIO_DESCRIPTION = "radio_description" CONTROLLER = "controller" +DATA_DEVICE_CONFIG = "zha_device_config" +DATA_ZHA = "zha" +DATA_ZHA_CONFIG = "config" +DATA_ZHA_BRIDGE_ID = "zha_bridge_id" +DATA_ZHA_CORE_EVENTS = "zha_core_events" +DATA_ZHA_DISPATCHERS = "zha_dispatchers" +DATA_ZHA_GATEWAY = "zha_gateway" + +DEBUG_COMP_BELLOWS = "bellows" +DEBUG_COMP_ZHA = "homeassistant.components.zha" +DEBUG_COMP_ZIGPY = "zigpy" +DEBUG_COMP_ZIGPY_DECONZ = "zigpy_deconz" +DEBUG_COMP_ZIGPY_XBEE = "zigpy_xbee" +DEBUG_LEVEL_CURRENT = "current" +DEBUG_LEVEL_ORIGINAL = "original" +DEBUG_LEVELS = { + DEBUG_COMP_BELLOWS: logging.DEBUG, + DEBUG_COMP_ZHA: logging.DEBUG, + DEBUG_COMP_ZIGPY: logging.DEBUG, + DEBUG_COMP_ZIGPY_XBEE: logging.DEBUG, + DEBUG_COMP_ZIGPY_DECONZ: logging.DEBUG, +} +DEBUG_RELAY_LOGGERS = [DEBUG_COMP_ZHA, DEBUG_COMP_ZIGPY] + DEFAULT_RADIO_TYPE = "ezsp" DEFAULT_BAUDRATE = 57600 DEFAULT_DATABASE_NAME = "zigbee.db" +DISCOVERY_KEY = "zha_discovery_info" -ATTR_CLUSTER_ID = "cluster_id" -ATTR_CLUSTER_TYPE = "cluster_type" -ATTR_ATTRIBUTE = "attribute" -ATTR_VALUE = "value" -ATTR_MANUFACTURER = "manufacturer" -ATTR_COMMAND = "command" -ATTR_COMMAND_TYPE = "command_type" -ATTR_ARGS = "args" -ATTR_ENDPOINT_ID = "endpoint_id" -ATTR_AVAILABLE = "available" +DOMAIN = "zha" -IN = "in" -OUT = "out" -CLIENT_COMMANDS = "client_commands" -SERVER_COMMANDS = "server_commands" -SERVER = "server" -IEEE = "ieee" -MODEL = "model" -NAME = "name" -LQI = "lqi" -RSSI = "rssi" -LAST_SEEN = "last_seen" - -SENSOR_TYPE = "sensor_type" -HUMIDITY = "humidity" -TEMPERATURE = "temperature" -ILLUMINANCE = "illuminance" -PRESSURE = "pressure" -METERING = "metering" -ELECTRICAL_MEASUREMENT = "electrical_measurement" -GENERIC = "generic" -BATTERY = "battery" -UNKNOWN = "unknown" -UNKNOWN_MANUFACTURER = "unk_manufacturer" -UNKNOWN_MODEL = "unk_model" -OPENING = "opening" -OCCUPANCY = "occupancy" -ACCELERATION = "acceleration" - -ATTR_LEVEL = "level" - -ZDO_CHANNEL = "zdo" -ON_OFF_CHANNEL = "on_off" -ATTRIBUTE_CHANNEL = "attribute" -BASIC_CHANNEL = "basic" -COLOR_CHANNEL = "light_color" -FAN_CHANNEL = "fan" -LEVEL_CHANNEL = ATTR_LEVEL -ZONE_CHANNEL = ZONE = "ias_zone" -ELECTRICAL_MEASUREMENT_CHANNEL = "electrical_measurement" -POWER_CONFIGURATION_CHANNEL = "power" -EVENT_RELAY_CHANNEL = "event_relay" -DOORLOCK_CHANNEL = "door_lock" - -SIGNAL_ATTR_UPDATED = "attribute_updated" -SIGNAL_MOVE_LEVEL = "move_level" -SIGNAL_SET_LEVEL = "set_level" -SIGNAL_STATE_ATTR = "update_state_attribute" -SIGNAL_AVAILABLE = "available" -SIGNAL_REMOVE = "remove" - -QUIRK_APPLIED = "quirk_applied" -QUIRK_CLASS = "quirk_class" -MANUFACTURER_CODE = "manufacturer_code" -POWER_SOURCE = "power_source" -MAINS_POWERED = "Mains" -BATTERY_OR_UNKNOWN = "Battery or Unknown" - -BELLOWS = "bellows" -ZHA = "homeassistant.components.zha" -ZIGPY = "zigpy" -ZIGPY_XBEE = "zigpy_xbee" -ZIGPY_DECONZ = "zigpy_deconz" -ORIGINAL = "original" -CURRENT = "current" -DEBUG_LEVELS = { - BELLOWS: logging.DEBUG, - ZHA: logging.DEBUG, - ZIGPY: logging.DEBUG, - ZIGPY_XBEE: logging.DEBUG, - ZIGPY_DECONZ: logging.DEBUG, -} -ADD_DEVICE_RELAY_LOGGERS = [ZHA, ZIGPY] -TYPE = "type" -NWK = "nwk" -SIGNATURE = "signature" -RAW_INIT = "raw_device_initialized" -ZHA_GW_MSG = "zha_gateway_message" -DEVICE_REMOVED = "device_removed" -DEVICE_INFO = "device_info" -DEVICE_FULL_INIT = "device_fully_initialized" -DEVICE_JOINED = "device_joined" -LOG_OUTPUT = "log_output" -LOG_ENTRY = "log_entry" MFG_CLUSTER_ID_START = 0xFC00 +POWER_MAINS_POWERED = "Mains" +POWER_BATTERY_OR_UNKNOWN = "Battery or Unknown" + class RadioType(enum.Enum): """Possible options for radio type.""" @@ -150,8 +116,6 @@ class RadioType(enum.Enum): return [e.value for e in RadioType] -DISCOVERY_KEY = "zha_discovery_info" - REPORT_CONFIG_MAX_INT = 900 REPORT_CONFIG_MAX_INT_BATTERY_SAVE = 10800 REPORT_CONFIG_MIN_INT = 30 @@ -185,3 +149,39 @@ REPORT_CONFIG_OP = ( REPORT_CONFIG_MAX_INT, REPORT_CONFIG_RPT_CHANGE, ) + +SENSOR_ACCELERATION = "acceleration" +SENSOR_BATTERY = "battery" +SENSOR_ELECTRICAL_MEASUREMENT = "electrical_measurement" +SENSOR_GENERIC = "generic" +SENSOR_HUMIDITY = "humidity" +SENSOR_ILLUMINANCE = "illuminance" +SENSOR_METERING = "metering" +SENSOR_OCCUPANCY = "occupancy" +SENSOR_OPENING = "opening" +SENSOR_PRESSURE = "pressure" +SENSOR_TEMPERATURE = "temperature" +SENSOR_TYPE = "sensor_type" + +SIGNAL_ATTR_UPDATED = "attribute_updated" +SIGNAL_AVAILABLE = "available" +SIGNAL_MOVE_LEVEL = "move_level" +SIGNAL_REMOVE = "remove" +SIGNAL_SET_LEVEL = "set_level" +SIGNAL_STATE_ATTR = "update_state_attribute" + +UNKNOWN = "unknown" +UNKNOWN_MANUFACTURER = "unk_manufacturer" +UNKNOWN_MODEL = "unk_model" + +ZHA_DISCOVERY_NEW = "zha_discovery_new_{}" +ZHA_GW_MSG_RAW_INIT = "raw_device_initialized" +ZHA_GW_MSG = "zha_gateway_message" +ZHA_GW_MSG_DEVICE_REMOVED = "device_removed" +ZHA_GW_MSG_DEVICE_INFO = "device_info" +ZHA_GW_MSG_DEVICE_FULL_INIT = "device_fully_initialized" +ZHA_GW_MSG_DEVICE_JOINED = "device_joined" +ZHA_GW_MSG_LOG_OUTPUT = "log_output" +ZHA_GW_MSG_LOG_ENTRY = "log_entry" +ZHA_GW_RADIO = "radio" +ZHA_GW_RADIO_DESCRIPTION = "radio_description" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index a76c580a3f1..748a9839bb2 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -21,36 +21,36 @@ from .channels import EventRelayChannel from .const import ( ATTR_ARGS, ATTR_ATTRIBUTE, + ATTR_AVAILABLE, ATTR_CLUSTER_ID, ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ENDPOINT_ID, ATTR_MANUFACTURER, ATTR_VALUE, - BATTERY_OR_UNKNOWN, - CLIENT_COMMANDS, - IEEE, - IN, - MAINS_POWERED, - MANUFACTURER_CODE, - MODEL, - NAME, - NWK, - OUT, - POWER_CONFIGURATION_CHANNEL, - POWER_SOURCE, - QUIRK_APPLIED, - QUIRK_CLASS, - SERVER, - SERVER_COMMANDS, + POWER_BATTERY_OR_UNKNOWN, + CLUSTER_COMMANDS_CLIENT, + ATTR_IEEE, + CLUSTER_TYPE_IN, + ATTR_LAST_SEEN, + ATTR_LQI, + POWER_MAINS_POWERED, + ATTR_MANUFACTURER_CODE, + ATTR_MODEL, + ATTR_NAME, + ATTR_NWK, + CLUSTER_TYPE_OUT, + CHANNEL_POWER_CONFIGURATION, + ATTR_POWER_SOURCE, + ATTR_QUIRK_APPLIED, + ATTR_QUIRK_CLASS, + ATTR_RSSI, + CLUSTER_COMMAND_SERVER, + CLUSTER_COMMANDS_SERVER, SIGNAL_AVAILABLE, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, - ZDO_CHANNEL, - LQI, - RSSI, - LAST_SEEN, - ATTR_AVAILABLE, + CHANNEL_ZDO, ) from .helpers import LogMixin @@ -155,7 +155,9 @@ class ZHADevice(LogMixin): @property def power_source(self): """Return the power source for the device.""" - return MAINS_POWERED if self.is_mains_powered else BATTERY_OR_UNKNOWN + return ( + POWER_MAINS_POWERED if self.is_mains_powered else POWER_BATTERY_OR_UNKNOWN + ) @property def is_router(self): @@ -223,18 +225,18 @@ class ZHADevice(LogMixin): time_struct = time.localtime(self.last_seen) update_time = time.strftime("%Y-%m-%dT%H:%M:%S", time_struct) return { - IEEE: ieee, - NWK: self.nwk, + ATTR_IEEE: ieee, + ATTR_NWK: self.nwk, ATTR_MANUFACTURER: self.manufacturer, - MODEL: self.model, - NAME: self.name or ieee, - QUIRK_APPLIED: self.quirk_applied, - QUIRK_CLASS: self.quirk_class, - MANUFACTURER_CODE: self.manufacturer_code, - POWER_SOURCE: self.power_source, - LQI: self.lqi, - RSSI: self.rssi, - LAST_SEEN: update_time, + ATTR_MODEL: self.model, + ATTR_NAME: self.name or ieee, + ATTR_QUIRK_APPLIED: self.quirk_applied, + ATTR_QUIRK_CLASS: self.quirk_class, + ATTR_MANUFACTURER_CODE: self.manufacturer_code, + ATTR_POWER_SOURCE: self.power_source, + ATTR_LQI: self.lqi, + ATTR_RSSI: self.rssi, + ATTR_LAST_SEEN: update_time, ATTR_AVAILABLE: self.available, } @@ -242,8 +244,8 @@ class ZHADevice(LogMixin): """Add cluster channel to device.""" # only keep 1 power configuration channel if ( - cluster_channel.name is POWER_CONFIGURATION_CHANNEL - and POWER_CONFIGURATION_CHANNEL in self.cluster_channels + cluster_channel.name is CHANNEL_POWER_CONFIGURATION + and CHANNEL_POWER_CONFIGURATION in self.cluster_channels ): return @@ -318,7 +320,7 @@ class ZHADevice(LogMixin): semaphore = asyncio.Semaphore(3) zdo_task = None for channel in channels: - if channel.name == ZDO_CHANNEL: + if channel.name == CHANNEL_ZDO: # pylint: disable=E1111 if zdo_task is None: # We only want to do this once zdo_task = self._async_create_task( @@ -356,7 +358,10 @@ class ZHADevice(LogMixin): def async_get_clusters(self): """Get all clusters for this device.""" return { - ep_id: {IN: endpoint.in_clusters, OUT: endpoint.out_clusters} + ep_id: { + CLUSTER_TYPE_IN: endpoint.in_clusters, + CLUSTER_TYPE_OUT: endpoint.out_clusters, + } for (ep_id, endpoint) in self._zigpy_device.endpoints.items() if ep_id != 0 } @@ -367,19 +372,24 @@ class ZHADevice(LogMixin): from zigpy.profiles import zha, zll return { - ep_id: {IN: endpoint.in_clusters, OUT: endpoint.out_clusters} + ep_id: { + CLUSTER_TYPE_IN: endpoint.in_clusters, + CLUSTER_TYPE_OUT: endpoint.out_clusters, + } for (ep_id, endpoint) in self._zigpy_device.endpoints.items() if ep_id != 0 and endpoint.profile_id in (zha.PROFILE_ID, zll.PROFILE_ID) } @callback - def async_get_cluster(self, endpoint_id, cluster_id, cluster_type=IN): + def async_get_cluster(self, endpoint_id, cluster_id, cluster_type=CLUSTER_TYPE_IN): """Get zigbee cluster from this entity.""" clusters = self.async_get_clusters() return clusters[endpoint_id][cluster_type][cluster_id] @callback - def async_get_cluster_attributes(self, endpoint_id, cluster_id, cluster_type=IN): + def async_get_cluster_attributes( + self, endpoint_id, cluster_id, cluster_type=CLUSTER_TYPE_IN + ): """Get zigbee attributes for specified cluster.""" cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: @@ -387,14 +397,16 @@ class ZHADevice(LogMixin): return cluster.attributes @callback - def async_get_cluster_commands(self, endpoint_id, cluster_id, cluster_type=IN): + def async_get_cluster_commands( + self, endpoint_id, cluster_id, cluster_type=CLUSTER_TYPE_IN + ): """Get zigbee commands for specified cluster.""" cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: return None return { - CLIENT_COMMANDS: cluster.client_commands, - SERVER_COMMANDS: cluster.server_commands, + CLUSTER_COMMANDS_CLIENT: cluster.client_commands, + CLUSTER_COMMANDS_SERVER: cluster.server_commands, } async def write_zigbee_attribute( @@ -403,7 +415,7 @@ class ZHADevice(LogMixin): cluster_id, attribute, value, - cluster_type=IN, + cluster_type=CLUSTER_TYPE_IN, manufacturer=None, ): """Write a value to a zigbee attribute for a cluster in this entity.""" @@ -444,7 +456,7 @@ class ZHADevice(LogMixin): command, command_type, args, - cluster_type=IN, + cluster_type=CLUSTER_TYPE_IN, manufacturer=None, ): """Issue a command against specified zigbee cluster on this entity.""" @@ -452,7 +464,7 @@ class ZHADevice(LogMixin): if cluster is None: return None response = None - if command_type == SERVER: + if command_type == CLUSTER_COMMAND_SERVER: response = await cluster.command( command, *args, manufacturer=manufacturer, expect_reply=True ) diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 97506494387..687728b5e26 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -12,28 +12,29 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send + from .channels import AttributeListeningChannel, EventRelayChannel, ZDOChannel from .channels.registry import ZIGBEE_CHANNEL_REGISTRY from .const import ( - CONF_DEVICE_CONFIG, COMPONENTS, - ZHA_DISCOVERY_NEW, + CONF_DEVICE_CONFIG, DATA_ZHA, + SENSOR_GENERIC, SENSOR_TYPE, UNKNOWN, - GENERIC, + ZHA_DISCOVERY_NEW, ) from .registries import ( BINARY_SENSOR_TYPES, CHANNEL_ONLY_CLUSTERS, - EVENT_RELAY_CLUSTERS, - SENSOR_TYPES, - DEVICE_CLASS, COMPONENT_CLUSTERS, - SINGLE_INPUT_CLUSTER_DEVICE_CLASS, - SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, + DEVICE_CLASS, + EVENT_RELAY_CLUSTERS, OUTPUT_CHANNEL_ONLY_CLUSTERS, REMOTE_DEVICE_TYPES, + SENSOR_TYPES, + SINGLE_INPUT_CLUSTER_DEVICE_CLASS, + SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, ) _LOGGER = logging.getLogger(__name__) @@ -291,7 +292,7 @@ def _async_handle_single_cluster_match( if component == SENSOR: discovery_info.update( - {SENSOR_TYPE: SENSOR_TYPES.get(cluster.cluster_id, GENERIC)} + {SENSOR_TYPE: SENSOR_TYPES.get(cluster.cluster_id, SENSOR_GENERIC)} ) if component == BINARY_SENSOR: discovery_info.update( diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 9994c445dbc..1e6367898d5 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -22,45 +22,45 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from ..api import async_get_device_info from .const import ( - ADD_DEVICE_RELAY_LOGGERS, + DEBUG_RELAY_LOGGERS, ATTR_MANUFACTURER, - BELLOWS, + DEBUG_COMP_BELLOWS, CONF_BAUDRATE, CONF_DATABASE, CONF_RADIO_TYPE, CONF_USB_PATH, CONTROLLER, - CURRENT, + DEBUG_LEVEL_CURRENT, DATA_ZHA, DATA_ZHA_BRIDGE_ID, DATA_ZHA_GATEWAY, DEBUG_LEVELS, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME, - DEVICE_FULL_INIT, - DEVICE_INFO, - DEVICE_JOINED, - DEVICE_REMOVED, + ZHA_GW_MSG_DEVICE_FULL_INIT, + ZHA_GW_MSG_DEVICE_INFO, + ZHA_GW_MSG_DEVICE_JOINED, + ZHA_GW_MSG_DEVICE_REMOVED, DOMAIN, - IEEE, - LOG_ENTRY, - LOG_OUTPUT, - MODEL, - NWK, - ORIGINAL, - RADIO, - RADIO_DESCRIPTION, - RAW_INIT, + ATTR_IEEE, + ZHA_GW_MSG_LOG_ENTRY, + ZHA_GW_MSG_LOG_OUTPUT, + ATTR_MODEL, + ATTR_NWK, + DEBUG_LEVEL_ORIGINAL, + ZHA_GW_RADIO, + ZHA_GW_RADIO_DESCRIPTION, + ZHA_GW_MSG_RAW_INIT, SIGNAL_REMOVE, - SIGNATURE, - TYPE, + ATTR_SIGNATURE, + ATTR_TYPE, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, - ZHA, + DEBUG_COMP_ZHA, ZHA_GW_MSG, - ZIGPY, - ZIGPY_DECONZ, - ZIGPY_XBEE, + DEBUG_COMP_ZIGPY, + DEBUG_COMP_ZIGPY_DECONZ, + DEBUG_COMP_ZIGPY_XBEE, ) from .device import DeviceStatus, ZHADevice from .discovery import async_dispatch_discovery_info, async_process_endpoint @@ -91,8 +91,8 @@ class ZHAGateway: self.radio_description = None hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self self._log_levels = { - ORIGINAL: async_capture_log_levels(), - CURRENT: async_capture_log_levels(), + DEBUG_LEVEL_ORIGINAL: async_capture_log_levels(), + DEBUG_LEVEL_CURRENT: async_capture_log_levels(), } self.debug_enabled = False self._log_relay_handler = LogRelayHandler(hass, self) @@ -107,9 +107,9 @@ class ZHAGateway: baudrate = self._config.get(CONF_BAUDRATE, DEFAULT_BAUDRATE) radio_type = self._config_entry.data.get(CONF_RADIO_TYPE) - radio_details = RADIO_TYPES[radio_type][RADIO]() - radio = radio_details[RADIO] - self.radio_description = RADIO_TYPES[radio_type][RADIO_DESCRIPTION] + radio_details = RADIO_TYPES[radio_type][ZHA_GW_RADIO]() + radio = radio_details[ZHA_GW_RADIO] + self.radio_description = RADIO_TYPES[radio_type][ZHA_GW_RADIO_DESCRIPTION] await radio.connect(usb_path, baudrate) if CONF_DATABASE in self._config: @@ -141,7 +141,11 @@ class ZHAGateway: async_dispatcher_send( self._hass, ZHA_GW_MSG, - {TYPE: DEVICE_JOINED, NWK: device.nwk, IEEE: str(device.ieee)}, + { + ATTR_TYPE: ZHA_GW_MSG_DEVICE_JOINED, + ATTR_NWK: device.nwk, + ATTR_IEEE: str(device.ieee), + }, ) def raw_device_initialized(self, device): @@ -154,12 +158,12 @@ class ZHAGateway: self._hass, ZHA_GW_MSG, { - TYPE: RAW_INIT, - NWK: device.nwk, - IEEE: str(device.ieee), - MODEL: device.model if device.model else UNKNOWN_MODEL, + ATTR_TYPE: ZHA_GW_MSG_RAW_INIT, + ATTR_NWK: device.nwk, + ATTR_IEEE: str(device.ieee), + ATTR_MODEL: device.model if device.model else UNKNOWN_MODEL, ATTR_MANUFACTURER: manuf if manuf else UNKNOWN_MANUFACTURER, - SIGNATURE: device.get_signature(), + ATTR_SIGNATURE: device.get_signature(), }, ) @@ -198,7 +202,10 @@ class ZHAGateway: async_dispatcher_send( self._hass, ZHA_GW_MSG, - {TYPE: DEVICE_REMOVED, DEVICE_INFO: device_info}, + { + ATTR_TYPE: ZHA_GW_MSG_DEVICE_REMOVED, + ZHA_GW_MSG_DEVICE_INFO: device_info, + }, ) def get_device(self, ieee): @@ -254,11 +261,11 @@ class ZHAGateway: @callback def async_enable_debug_mode(self): """Enable debug mode for ZHA.""" - self._log_levels[ORIGINAL] = async_capture_log_levels() + self._log_levels[DEBUG_LEVEL_ORIGINAL] = async_capture_log_levels() async_set_logger_levels(DEBUG_LEVELS) - self._log_levels[CURRENT] = async_capture_log_levels() + self._log_levels[DEBUG_LEVEL_CURRENT] = async_capture_log_levels() - for logger_name in ADD_DEVICE_RELAY_LOGGERS: + for logger_name in DEBUG_RELAY_LOGGERS: logging.getLogger(logger_name).addHandler(self._log_relay_handler) self.debug_enabled = True @@ -266,9 +273,9 @@ class ZHAGateway: @callback def async_disable_debug_mode(self): """Disable debug mode for ZHA.""" - async_set_logger_levels(self._log_levels[ORIGINAL]) - self._log_levels[CURRENT] = async_capture_log_levels() - for logger_name in ADD_DEVICE_RELAY_LOGGERS: + async_set_logger_levels(self._log_levels[DEBUG_LEVEL_ORIGINAL]) + self._log_levels[DEBUG_LEVEL_CURRENT] = async_capture_log_levels() + for logger_name in DEBUG_RELAY_LOGGERS: logging.getLogger(logger_name).removeHandler(self._log_relay_handler) self.debug_enabled = False @@ -377,7 +384,10 @@ class ZHAGateway: async_dispatcher_send( self._hass, ZHA_GW_MSG, - {TYPE: DEVICE_FULL_INIT, DEVICE_INFO: device_info}, + { + ATTR_TYPE: ZHA_GW_MSG_DEVICE_FULL_INIT, + ZHA_GW_MSG_DEVICE_INFO: device_info, + }, ) async def shutdown(self): @@ -390,22 +400,26 @@ class ZHAGateway: def async_capture_log_levels(): """Capture current logger levels for ZHA.""" return { - BELLOWS: logging.getLogger(BELLOWS).getEffectiveLevel(), - ZHA: logging.getLogger(ZHA).getEffectiveLevel(), - ZIGPY: logging.getLogger(ZIGPY).getEffectiveLevel(), - ZIGPY_XBEE: logging.getLogger(ZIGPY_XBEE).getEffectiveLevel(), - ZIGPY_DECONZ: logging.getLogger(ZIGPY_DECONZ).getEffectiveLevel(), + DEBUG_COMP_BELLOWS: logging.getLogger(DEBUG_COMP_BELLOWS).getEffectiveLevel(), + DEBUG_COMP_ZHA: logging.getLogger(DEBUG_COMP_ZHA).getEffectiveLevel(), + DEBUG_COMP_ZIGPY: logging.getLogger(DEBUG_COMP_ZIGPY).getEffectiveLevel(), + DEBUG_COMP_ZIGPY_XBEE: logging.getLogger( + DEBUG_COMP_ZIGPY_XBEE + ).getEffectiveLevel(), + DEBUG_COMP_ZIGPY_DECONZ: logging.getLogger( + DEBUG_COMP_ZIGPY_DECONZ + ).getEffectiveLevel(), } @callback def async_set_logger_levels(levels): """Set logger levels for ZHA.""" - logging.getLogger(BELLOWS).setLevel(levels[BELLOWS]) - logging.getLogger(ZHA).setLevel(levels[ZHA]) - logging.getLogger(ZIGPY).setLevel(levels[ZIGPY]) - logging.getLogger(ZIGPY_XBEE).setLevel(levels[ZIGPY_XBEE]) - logging.getLogger(ZIGPY_DECONZ).setLevel(levels[ZIGPY_DECONZ]) + logging.getLogger(DEBUG_COMP_BELLOWS).setLevel(levels[DEBUG_COMP_BELLOWS]) + logging.getLogger(DEBUG_COMP_ZHA).setLevel(levels[DEBUG_COMP_ZHA]) + logging.getLogger(DEBUG_COMP_ZIGPY).setLevel(levels[DEBUG_COMP_ZIGPY]) + logging.getLogger(DEBUG_COMP_ZIGPY_XBEE).setLevel(levels[DEBUG_COMP_ZIGPY_XBEE]) + logging.getLogger(DEBUG_COMP_ZIGPY_DECONZ).setLevel(levels[DEBUG_COMP_ZIGPY_DECONZ]) class LogRelayHandler(logging.Handler): @@ -426,5 +440,7 @@ class LogRelayHandler(logging.Handler): entry = LogEntry(record, stack, _figure_out_source(record, stack, self.hass)) async_dispatcher_send( - self.hass, ZHA_GW_MSG, {TYPE: LOG_OUTPUT, LOG_ENTRY: entry.to_dict()} + self.hass, + ZHA_GW_MSG, + {ATTR_TYPE: ZHA_GW_MSG_LOG_OUTPUT, ZHA_GW_MSG_LOG_ENTRY: entry.to_dict()}, ) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 3a053d7be01..23ebd9381bb 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -6,17 +6,19 @@ https://home-assistant.io/components/zha/ """ import asyncio import collections -import logging from concurrent.futures import TimeoutError as Timeout +import logging + from homeassistant.core import callback + from .const import ( DEFAULT_BAUDRATE, + CLUSTER_TYPE_IN, + CLUSTER_TYPE_OUT, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MIN_INT, REPORT_CONFIG_RPT_CHANGE, RadioType, - IN, - OUT, ) from .registries import BINDABLE_CLUSTERS @@ -206,14 +208,18 @@ async def get_matched_clusters(source_zha_device, target_zha_device): clusters_to_bind = [] for endpoint_id in source_clusters: - for cluster_id in source_clusters[endpoint_id][OUT]: + for cluster_id in source_clusters[endpoint_id][CLUSTER_TYPE_OUT]: if cluster_id not in BINDABLE_CLUSTERS: continue for t_endpoint_id in target_clusters: - if cluster_id in target_clusters[t_endpoint_id][IN]: + if cluster_id in target_clusters[t_endpoint_id][CLUSTER_TYPE_IN]: cluster_pair = ClusterPair( - source_cluster=source_clusters[endpoint_id][OUT][cluster_id], - target_cluster=target_clusters[t_endpoint_id][IN][cluster_id], + source_cluster=source_clusters[endpoint_id][CLUSTER_TYPE_OUT][ + cluster_id + ], + target_cluster=target_clusters[t_endpoint_id][CLUSTER_TYPE_IN][ + cluster_id + ], ) clusters_to_bind.append(cluster_pair) return clusters_to_bind @@ -228,9 +234,9 @@ def async_is_bindable_target(source_zha_device, target_zha_device): bindables = set(BINDABLE_CLUSTERS) for endpoint_id in source_clusters: for t_endpoint_id in target_clusters: - matches = set(source_clusters[endpoint_id][OUT].keys()).intersection( - target_clusters[t_endpoint_id][IN].keys() - ) + matches = set( + source_clusters[endpoint_id][CLUSTER_TYPE_OUT].keys() + ).intersection(target_clusters[t_endpoint_id][CLUSTER_TYPE_IN].keys()) if any(bindable in bindables for bindable in matches): return True return False diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index e0cfbeeee7b..27e79749838 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -14,56 +14,56 @@ from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH from .const import ( - HUMIDITY, - TEMPERATURE, - ILLUMINANCE, - PRESSURE, - METERING, - ELECTRICAL_MEASUREMENT, - OCCUPANCY, - REPORT_CONFIG_IMMEDIATE, - OPENING, - ZONE, - RADIO_DESCRIPTION, + SENSOR_ACCELERATION, + SENSOR_BATTERY, + CONTROLLER, + SENSOR_ELECTRICAL_MEASUREMENT, + SENSOR_HUMIDITY, + SENSOR_ILLUMINANCE, + SENSOR_METERING, + SENSOR_OCCUPANCY, + SENSOR_OPENING, + SENSOR_PRESSURE, + ZHA_GW_RADIO, + ZHA_GW_RADIO_DESCRIPTION, REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, - REPORT_CONFIG_MIN_INT, + REPORT_CONFIG_IMMEDIATE, REPORT_CONFIG_MAX_INT, + REPORT_CONFIG_MIN_INT, REPORT_CONFIG_OP, - ACCELERATION, + SENSOR_TEMPERATURE, + ZONE, RadioType, - RADIO, - CONTROLLER, - BATTERY, ) -SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45 -SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02 -SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000 - -DEVICE_CLASS = {} -SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {} -SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {} -SENSOR_TYPES = {} -RADIO_TYPES = {} +BINARY_SENSOR_CLUSTERS = set() BINARY_SENSOR_TYPES = {} -REMOTE_DEVICE_TYPES = {} +BINDABLE_CLUSTERS = [] +CHANNEL_ONLY_CLUSTERS = [] CLUSTER_REPORT_CONFIGS = {} CUSTOM_CLUSTER_MAPPINGS = {} -EVENT_RELAY_CLUSTERS = [] -CHANNEL_ONLY_CLUSTERS = [] -OUTPUT_CHANNEL_ONLY_CLUSTERS = [] -BINDABLE_CLUSTERS = [] -INPUT_BIND_ONLY_CLUSTERS = [] -BINARY_SENSOR_CLUSTERS = set() +DEVICE_CLASS = {} DEVICE_TRACKER_CLUSTERS = set() +EVENT_RELAY_CLUSTERS = [] +INPUT_BIND_ONLY_CLUSTERS = [] LIGHT_CLUSTERS = set() +OUTPUT_CHANNEL_ONLY_CLUSTERS = [] +RADIO_TYPES = {} +REMOTE_DEVICE_TYPES = {} +SENSOR_TYPES = {} +SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {} +SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {} SWITCH_CLUSTERS = set() +SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02 +SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000 +SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45 + COMPONENT_CLUSTERS = { BINARY_SENSOR: BINARY_SENSOR_CLUSTERS, + DEVICE_TRACKER: DEVICE_TRACKER_CLUSTERS, LIGHT: LIGHT_CLUSTERS, SWITCH: SWITCH_CLUSTERS, - DEVICE_TRACKER: DEVICE_TRACKER_CLUSTERS, } @@ -90,144 +90,58 @@ def establish_device_mappings(): import bellows.ezsp from bellows.zigbee.application import ControllerApplication - return {RADIO: bellows.ezsp.EZSP(), CONTROLLER: ControllerApplication} + return {ZHA_GW_RADIO: bellows.ezsp.EZSP(), CONTROLLER: ControllerApplication} RADIO_TYPES[RadioType.ezsp.name] = { - RADIO: get_ezsp_radio, - RADIO_DESCRIPTION: "EZSP", + ZHA_GW_RADIO: get_ezsp_radio, + ZHA_GW_RADIO_DESCRIPTION: "EZSP", } def get_xbee_radio(): import zigpy_xbee.api from zigpy_xbee.zigbee.application import ControllerApplication - return {RADIO: zigpy_xbee.api.XBee(), CONTROLLER: ControllerApplication} + return {ZHA_GW_RADIO: zigpy_xbee.api.XBee(), CONTROLLER: ControllerApplication} RADIO_TYPES[RadioType.xbee.name] = { - RADIO: get_xbee_radio, - RADIO_DESCRIPTION: "XBee", + ZHA_GW_RADIO: get_xbee_radio, + ZHA_GW_RADIO_DESCRIPTION: "XBee", } def get_deconz_radio(): import zigpy_deconz.api from zigpy_deconz.zigbee.application import ControllerApplication - return {RADIO: zigpy_deconz.api.Deconz(), CONTROLLER: ControllerApplication} + return { + ZHA_GW_RADIO: zigpy_deconz.api.Deconz(), + CONTROLLER: ControllerApplication, + } RADIO_TYPES[RadioType.deconz.name] = { - RADIO: get_deconz_radio, - RADIO_DESCRIPTION: "Deconz", + ZHA_GW_RADIO: get_deconz_radio, + ZHA_GW_RADIO_DESCRIPTION: "Deconz", } - EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id) - EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id) + BINARY_SENSOR_CLUSTERS.add(SMARTTHINGS_ACCELERATION_CLUSTER) + BINARY_SENSOR_CLUSTERS.add(zcl.clusters.general.OnOff.cluster_id) + BINARY_SENSOR_CLUSTERS.add(zcl.clusters.measurement.OccupancySensing.cluster_id) + BINARY_SENSOR_CLUSTERS.add(zcl.clusters.security.IasZone.cluster_id) - CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.general.Basic.cluster_id) - CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.lightlink.LightLink.cluster_id) - - OUTPUT_CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.general.Scenes.cluster_id) + BINARY_SENSOR_TYPES.update( + { + SMARTTHINGS_ACCELERATION_CLUSTER: SENSOR_ACCELERATION, + zcl.clusters.general.OnOff.cluster_id: SENSOR_OPENING, + zcl.clusters.measurement.OccupancySensing.cluster_id: SENSOR_OCCUPANCY, + zcl.clusters.security.IasZone.cluster_id: ZONE, + } + ) BINDABLE_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id) BINDABLE_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id) BINDABLE_CLUSTERS.append(zcl.clusters.lighting.Color.cluster_id) - INPUT_BIND_ONLY_CLUSTERS.append(zcl.clusters.lightlink.LightLink.cluster_id) - - DEVICE_CLASS[zha.PROFILE_ID].update( - { - zha.DeviceType.SMART_PLUG: SWITCH, - zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: LIGHT, - zha.DeviceType.ON_OFF_LIGHT: LIGHT, - zha.DeviceType.DIMMABLE_LIGHT: LIGHT, - zha.DeviceType.COLOR_DIMMABLE_LIGHT: LIGHT, - zha.DeviceType.ON_OFF_LIGHT_SWITCH: SWITCH, - zha.DeviceType.ON_OFF_BALLAST: SWITCH, - zha.DeviceType.DIMMABLE_BALLAST: LIGHT, - zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH, - zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT, - zha.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, - zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, - SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE: DEVICE_TRACKER, - } - ) - - DEVICE_CLASS[zll.PROFILE_ID].update( - { - zll.DeviceType.ON_OFF_LIGHT: LIGHT, - zll.DeviceType.ON_OFF_PLUGIN_UNIT: SWITCH, - zll.DeviceType.DIMMABLE_LIGHT: LIGHT, - zll.DeviceType.DIMMABLE_PLUGIN_UNIT: LIGHT, - zll.DeviceType.COLOR_LIGHT: LIGHT, - zll.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, - zll.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, - } - ) - - SINGLE_INPUT_CLUSTER_DEVICE_CLASS.update( - { - zcl.clusters.general.OnOff: SWITCH, - zcl.clusters.measurement.RelativeHumidity: SENSOR, - # this works for now but if we hit conflicts we can break it out to - # a different dict that is keyed by manufacturer - SMARTTHINGS_HUMIDITY_CLUSTER: SENSOR, - zcl.clusters.measurement.TemperatureMeasurement: SENSOR, - zcl.clusters.measurement.PressureMeasurement: SENSOR, - zcl.clusters.measurement.IlluminanceMeasurement: SENSOR, - zcl.clusters.smartenergy.Metering: SENSOR, - zcl.clusters.homeautomation.ElectricalMeasurement: SENSOR, - zcl.clusters.security.IasZone: BINARY_SENSOR, - zcl.clusters.measurement.OccupancySensing: BINARY_SENSOR, - zcl.clusters.hvac.Fan: FAN, - SMARTTHINGS_ACCELERATION_CLUSTER: BINARY_SENSOR, - zcl.clusters.general.MultistateInput.cluster_id: SENSOR, - zcl.clusters.general.AnalogInput.cluster_id: SENSOR, - zcl.clusters.closures.DoorLock: LOCK, - zcl.clusters.general.PowerConfiguration: SENSOR, - } - ) - - SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update( - {zcl.clusters.general.OnOff: BINARY_SENSOR} - ) - - SENSOR_TYPES.update( - { - zcl.clusters.measurement.RelativeHumidity.cluster_id: HUMIDITY, - SMARTTHINGS_HUMIDITY_CLUSTER: HUMIDITY, - zcl.clusters.measurement.TemperatureMeasurement.cluster_id: TEMPERATURE, - zcl.clusters.measurement.PressureMeasurement.cluster_id: PRESSURE, - zcl.clusters.measurement.IlluminanceMeasurement.cluster_id: ILLUMINANCE, - zcl.clusters.smartenergy.Metering.cluster_id: METERING, - zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: ELECTRICAL_MEASUREMENT, - zcl.clusters.general.PowerConfiguration.cluster_id: BATTERY, - } - ) - - BINARY_SENSOR_TYPES.update( - { - zcl.clusters.measurement.OccupancySensing.cluster_id: OCCUPANCY, - zcl.clusters.security.IasZone.cluster_id: ZONE, - zcl.clusters.general.OnOff.cluster_id: OPENING, - SMARTTHINGS_ACCELERATION_CLUSTER: ACCELERATION, - } - ) - - zhap = zha.PROFILE_ID - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.NON_COLOR_SCENE_CONTROLLER) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.NON_COLOR_CONTROLLER) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_SCENE_CONTROLLER) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_CONTROLLER) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.REMOTE_CONTROL) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.SCENE_SELECTOR) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.DIMMER_SWITCH) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_DIMMER_SWITCH) - - zllp = zll.PROFILE_ID - REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.COLOR_CONTROLLER) - REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.COLOR_SCENE_CONTROLLER) - REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.CONTROLLER) - REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.SCENE_CONTROLLER) - REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.CONTROL_BRIDGE) + CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.general.Basic.cluster_id) + CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.lightlink.LightLink.cluster_id) CLUSTER_REPORT_CONFIGS.update( { @@ -311,15 +225,104 @@ def establish_device_mappings(): } ) - BINARY_SENSOR_CLUSTERS.add(zcl.clusters.general.OnOff.cluster_id) - BINARY_SENSOR_CLUSTERS.add(zcl.clusters.security.IasZone.cluster_id) - BINARY_SENSOR_CLUSTERS.add(zcl.clusters.measurement.OccupancySensing.cluster_id) - BINARY_SENSOR_CLUSTERS.add(SMARTTHINGS_ACCELERATION_CLUSTER) + DEVICE_CLASS[zha.PROFILE_ID].update( + { + SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE: DEVICE_TRACKER, + zha.DeviceType.COLOR_DIMMABLE_LIGHT: LIGHT, + zha.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, + zha.DeviceType.DIMMABLE_BALLAST: LIGHT, + zha.DeviceType.DIMMABLE_LIGHT: LIGHT, + zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT, + zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, + zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: LIGHT, + zha.DeviceType.ON_OFF_BALLAST: SWITCH, + zha.DeviceType.ON_OFF_LIGHT: LIGHT, + zha.DeviceType.ON_OFF_LIGHT_SWITCH: SWITCH, + zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH, + zha.DeviceType.SMART_PLUG: SWITCH, + } + ) + + DEVICE_CLASS[zll.PROFILE_ID].update( + { + zll.DeviceType.COLOR_LIGHT: LIGHT, + zll.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, + zll.DeviceType.DIMMABLE_LIGHT: LIGHT, + zll.DeviceType.DIMMABLE_PLUGIN_UNIT: LIGHT, + zll.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, + zll.DeviceType.ON_OFF_LIGHT: LIGHT, + zll.DeviceType.ON_OFF_PLUGIN_UNIT: SWITCH, + } + ) DEVICE_TRACKER_CLUSTERS.add(zcl.clusters.general.PowerConfiguration.cluster_id) - LIGHT_CLUSTERS.add(zcl.clusters.general.OnOff.cluster_id) + EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id) + EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id) + + INPUT_BIND_ONLY_CLUSTERS.append(zcl.clusters.lightlink.LightLink.cluster_id) + + OUTPUT_CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.general.Scenes.cluster_id) + LIGHT_CLUSTERS.add(zcl.clusters.general.LevelControl.cluster_id) + LIGHT_CLUSTERS.add(zcl.clusters.general.OnOff.cluster_id) LIGHT_CLUSTERS.add(zcl.clusters.lighting.Color.cluster_id) + SINGLE_INPUT_CLUSTER_DEVICE_CLASS.update( + { + # this works for now but if we hit conflicts we can break it out to + # a different dict that is keyed by manufacturer + SMARTTHINGS_ACCELERATION_CLUSTER: BINARY_SENSOR, + SMARTTHINGS_HUMIDITY_CLUSTER: SENSOR, + zcl.clusters.closures.DoorLock: LOCK, + zcl.clusters.general.AnalogInput.cluster_id: SENSOR, + zcl.clusters.general.MultistateInput.cluster_id: SENSOR, + zcl.clusters.general.OnOff: SWITCH, + zcl.clusters.general.PowerConfiguration: SENSOR, + zcl.clusters.homeautomation.ElectricalMeasurement: SENSOR, + zcl.clusters.hvac.Fan: FAN, + zcl.clusters.measurement.IlluminanceMeasurement: SENSOR, + zcl.clusters.measurement.OccupancySensing: BINARY_SENSOR, + zcl.clusters.measurement.PressureMeasurement: SENSOR, + zcl.clusters.measurement.RelativeHumidity: SENSOR, + zcl.clusters.measurement.TemperatureMeasurement: SENSOR, + zcl.clusters.security.IasZone: BINARY_SENSOR, + zcl.clusters.smartenergy.Metering: SENSOR, + } + ) + + SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update( + {zcl.clusters.general.OnOff: BINARY_SENSOR} + ) + + SENSOR_TYPES.update( + { + SMARTTHINGS_HUMIDITY_CLUSTER: SENSOR_HUMIDITY, + zcl.clusters.general.PowerConfiguration.cluster_id: SENSOR_BATTERY, + zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: SENSOR_ELECTRICAL_MEASUREMENT, + zcl.clusters.measurement.IlluminanceMeasurement.cluster_id: SENSOR_ILLUMINANCE, + zcl.clusters.measurement.PressureMeasurement.cluster_id: SENSOR_PRESSURE, + zcl.clusters.measurement.RelativeHumidity.cluster_id: SENSOR_HUMIDITY, + zcl.clusters.measurement.TemperatureMeasurement.cluster_id: SENSOR_TEMPERATURE, + zcl.clusters.smartenergy.Metering.cluster_id: SENSOR_METERING, + } + ) + SWITCH_CLUSTERS.add(zcl.clusters.general.OnOff.cluster_id) + + zhap = zha.PROFILE_ID + REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_CONTROLLER) + REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_DIMMER_SWITCH) + REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_SCENE_CONTROLLER) + REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.DIMMER_SWITCH) + REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.NON_COLOR_CONTROLLER) + REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.NON_COLOR_SCENE_CONTROLLER) + REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.REMOTE_CONTROL) + REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.SCENE_SELECTOR) + + zllp = zll.PROFILE_ID + REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.COLOR_CONTROLLER) + REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.COLOR_SCENE_CONTROLLER) + REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.CONTROL_BRIDGE) + REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.CONTROLLER) + REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.SCENE_CONTROLLER) diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index 312cfc7e545..85b4261e4ec 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -1,16 +1,15 @@ """Data storage helper for ZHA.""" -import logging -from collections import OrderedDict - # pylint: disable=W0611 +from collections import OrderedDict +import logging from typing import MutableMapping # noqa: F401 from typing import cast import attr from homeassistant.core import callback -from homeassistant.loader import bind_hass from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index 6b881ff7a7d..5d05e980dab 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -1,16 +1,18 @@ """Support for the ZHA platform.""" import logging import time -from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER, DOMAIN + +from homeassistant.components.device_tracker import DOMAIN, SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .core.const import ( DATA_ZHA, DATA_ZHA_DISPATCHERS, - ZHA_DISCOVERY_NEW, - POWER_CONFIGURATION_CHANNEL, + CHANNEL_POWER_CONFIGURATION, SIGNAL_ATTR_UPDATED, + ZHA_DISCOVERY_NEW, ) from .entity import ZhaEntity from .sensor import battery_percentage_remaining_formatter @@ -56,7 +58,7 @@ class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity): def __init__(self, **kwargs): """Initialize the ZHA device tracker.""" super().__init__(**kwargs) - self._battery_channel = self.cluster_channels.get(POWER_CONFIGURATION_CHANNEL) + self._battery_channel = self.cluster_channels.get(CHANNEL_POWER_CONFIGURATION) self._connected = False self._keepalive_interval = 60 self._should_poll = True diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 46cb7583ef3..dd5364b09e0 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -16,8 +16,8 @@ from .core.const import ( DATA_ZHA, DATA_ZHA_BRIDGE_ID, DOMAIN, - MODEL, - NAME, + ATTR_MODEL, + ATTR_NAME, SIGNAL_REMOVE, ) from .core.helpers import LogMixin @@ -99,8 +99,8 @@ class ZhaEntity(RestoreEntity, LogMixin, entity.Entity): "connections": {(CONNECTION_ZIGBEE, ieee)}, "identifiers": {(DOMAIN, ieee)}, ATTR_MANUFACTURER: zha_device_info[ATTR_MANUFACTURER], - MODEL: zha_device_info[MODEL], - NAME: zha_device_info[NAME], + ATTR_MODEL: zha_device_info[ATTR_MODEL], + ATTR_NAME: zha_device_info[ATTR_NAME], "via_device": (DOMAIN, self.hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID]), } diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 318e2962e54..e20d147006a 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -1,7 +1,6 @@ """Fans on Zigbee Home Automation networks.""" import logging -from homeassistant.core import callback from homeassistant.components.fan import ( DOMAIN, SPEED_HIGH, @@ -11,13 +10,15 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .core.const import ( DATA_ZHA, DATA_ZHA_DISPATCHERS, - ZHA_DISCOVERY_NEW, - FAN_CHANNEL, + CHANNEL_FAN, SIGNAL_ATTR_UPDATED, + ZHA_DISCOVERY_NEW, ) from .entity import ZhaEntity @@ -91,7 +92,7 @@ class ZhaFan(ZhaEntity, FanEntity): def __init__(self, unique_id, zha_device, channels, **kwargs): """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) - self._fan_channel = self.cluster_channels.get(FAN_CHANNEL) + self._fan_channel = self.cluster_channels.get(CHANNEL_FAN) async def async_added_to_hass(self): """Run when about to be added to hass.""" diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 64b1897bb3a..80d6af81605 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -3,21 +3,23 @@ from datetime import timedelta import logging from zigpy.zcl.foundation import Status + from homeassistant.components import light from homeassistant.const import STATE_ON from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_time_interval import homeassistant.util.color as color_util + from .core.const import ( + CHANNEL_COLOR, DATA_ZHA, DATA_ZHA_DISPATCHERS, - ZHA_DISCOVERY_NEW, - COLOR_CHANNEL, - ON_OFF_CHANNEL, - LEVEL_CHANNEL, + CHANNEL_LEVEL, + CHANNEL_ON_OFF, SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL, + ZHA_DISCOVERY_NEW, ) from .entity import ZhaEntity @@ -83,9 +85,9 @@ class Light(ZhaEntity, light.Light): self._color_temp = None self._hs_color = None self._brightness = None - self._on_off_channel = self.cluster_channels.get(ON_OFF_CHANNEL) - self._level_channel = self.cluster_channels.get(LEVEL_CHANNEL) - self._color_channel = self.cluster_channels.get(COLOR_CHANNEL) + self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) + self._level_channel = self.cluster_channels.get(CHANNEL_LEVEL) + self._color_channel = self.cluster_channels.get(CHANNEL_COLOR) if self._level_channel: self._supported_features |= light.SUPPORT_BRIGHTNESS diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index f3c6326d32d..a7b2d393012 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -2,20 +2,22 @@ import logging from zigpy.zcl.foundation import Status -from homeassistant.core import callback + from homeassistant.components.lock import ( DOMAIN, - STATE_UNLOCKED, STATE_LOCKED, + STATE_UNLOCKED, LockDevice, ) +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .core.const import ( DATA_ZHA, DATA_ZHA_DISPATCHERS, - ZHA_DISCOVERY_NEW, - DOORLOCK_CHANNEL, + CHANNEL_DOORLOCK, SIGNAL_ATTR_UPDATED, + ZHA_DISCOVERY_NEW, ) from .entity import ZhaEntity @@ -73,7 +75,7 @@ class ZhaDoorLock(ZhaEntity, LockDevice): def __init__(self, unique_id, zha_device, channels, **kwargs): """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) - self._doorlock_channel = self.cluster_channels.get(DOORLOCK_CHANNEL) + self._doorlock_channel = self.cluster_channels.get(CHANNEL_DOORLOCK) async def async_added_to_hass(self): """Run when about to be added to hass.""" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index fe0096dd150..df8e2add7dd 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -2,37 +2,38 @@ import logging import numbers -from homeassistant.core import callback from homeassistant.components.sensor import ( - DOMAIN, + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_PRESSURE, DEVICE_CLASS_POWER, - DEVICE_CLASS_BATTERY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + DOMAIN, ) -from homeassistant.const import TEMP_CELSIUS, POWER_WATT, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, POWER_WATT, TEMP_CELSIUS +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .core.const import ( + CHANNEL_ATTRIBUTE, + SENSOR_BATTERY, DATA_ZHA, DATA_ZHA_DISPATCHERS, - ZHA_DISCOVERY_NEW, - HUMIDITY, - TEMPERATURE, - ILLUMINANCE, - PRESSURE, - METERING, - ELECTRICAL_MEASUREMENT, - GENERIC, + SENSOR_ELECTRICAL_MEASUREMENT, + CHANNEL_ELECTRICAL_MEASUREMENT, + SENSOR_GENERIC, + SENSOR_HUMIDITY, + SENSOR_ILLUMINANCE, + SENSOR_METERING, + CHANNEL_POWER_CONFIGURATION, + SENSOR_PRESSURE, SENSOR_TYPE, - ATTRIBUTE_CHANNEL, - ELECTRICAL_MEASUREMENT_CHANNEL, SIGNAL_ATTR_UPDATED, SIGNAL_STATE_ATTR, + SENSOR_TEMPERATURE, UNKNOWN, - BATTERY, - POWER_CONFIGURATION_CHANNEL, + ZHA_DISCOVERY_NEW, ) from .entity import ZhaEntity @@ -121,49 +122,49 @@ async def async_battery_device_state_attr_provider(channel): FORMATTER_FUNC_REGISTRY = { - HUMIDITY: humidity_formatter, - TEMPERATURE: temperature_formatter, - PRESSURE: pressure_formatter, - ELECTRICAL_MEASUREMENT: active_power_formatter, - ILLUMINANCE: illuminance_formatter, - GENERIC: pass_through_formatter, - BATTERY: battery_percentage_remaining_formatter, + SENSOR_HUMIDITY: humidity_formatter, + SENSOR_TEMPERATURE: temperature_formatter, + SENSOR_PRESSURE: pressure_formatter, + SENSOR_ELECTRICAL_MEASUREMENT: active_power_formatter, + SENSOR_ILLUMINANCE: illuminance_formatter, + SENSOR_GENERIC: pass_through_formatter, + SENSOR_BATTERY: battery_percentage_remaining_formatter, } UNIT_REGISTRY = { - HUMIDITY: "%", - TEMPERATURE: TEMP_CELSIUS, - PRESSURE: "hPa", - ILLUMINANCE: "lx", - METERING: POWER_WATT, - ELECTRICAL_MEASUREMENT: POWER_WATT, - GENERIC: None, - BATTERY: "%", + SENSOR_HUMIDITY: "%", + SENSOR_TEMPERATURE: TEMP_CELSIUS, + SENSOR_PRESSURE: "hPa", + SENSOR_ILLUMINANCE: "lx", + SENSOR_METERING: POWER_WATT, + SENSOR_ELECTRICAL_MEASUREMENT: POWER_WATT, + SENSOR_GENERIC: None, + SENSOR_BATTERY: "%", } CHANNEL_REGISTRY = { - ELECTRICAL_MEASUREMENT: ELECTRICAL_MEASUREMENT_CHANNEL, - BATTERY: POWER_CONFIGURATION_CHANNEL, + SENSOR_ELECTRICAL_MEASUREMENT: CHANNEL_ELECTRICAL_MEASUREMENT, + SENSOR_BATTERY: CHANNEL_POWER_CONFIGURATION, } -POLLING_REGISTRY = {ELECTRICAL_MEASUREMENT: True} +POLLING_REGISTRY = {SENSOR_ELECTRICAL_MEASUREMENT: True} -FORCE_UPDATE_REGISTRY = {ELECTRICAL_MEASUREMENT: False} +FORCE_UPDATE_REGISTRY = {SENSOR_ELECTRICAL_MEASUREMENT: False} DEVICE_CLASS_REGISTRY = { UNKNOWN: None, - HUMIDITY: DEVICE_CLASS_HUMIDITY, - TEMPERATURE: DEVICE_CLASS_TEMPERATURE, - PRESSURE: DEVICE_CLASS_PRESSURE, - ILLUMINANCE: DEVICE_CLASS_ILLUMINANCE, - METERING: DEVICE_CLASS_POWER, - ELECTRICAL_MEASUREMENT: DEVICE_CLASS_POWER, - BATTERY: DEVICE_CLASS_BATTERY, + SENSOR_HUMIDITY: DEVICE_CLASS_HUMIDITY, + SENSOR_TEMPERATURE: DEVICE_CLASS_TEMPERATURE, + SENSOR_PRESSURE: DEVICE_CLASS_PRESSURE, + SENSOR_ILLUMINANCE: DEVICE_CLASS_ILLUMINANCE, + SENSOR_METERING: DEVICE_CLASS_POWER, + SENSOR_ELECTRICAL_MEASUREMENT: DEVICE_CLASS_POWER, + SENSOR_BATTERY: DEVICE_CLASS_BATTERY, } DEVICE_STATE_ATTR_PROVIDER_REGISTRY = { - BATTERY: async_battery_device_state_attr_provider + SENSOR_BATTERY: async_battery_device_state_attr_provider } @@ -217,7 +218,7 @@ class Sensor(ZhaEntity): def __init__(self, unique_id, zha_device, channels, **kwargs): """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) - self._sensor_type = kwargs.get(SENSOR_TYPE, GENERIC) + self._sensor_type = kwargs.get(SENSOR_TYPE, SENSOR_GENERIC) self._unit = UNIT_REGISTRY.get(self._sensor_type) self._formatter_function = FORMATTER_FUNC_REGISTRY.get( self._sensor_type, pass_through_formatter @@ -225,7 +226,7 @@ class Sensor(ZhaEntity): self._force_update = FORCE_UPDATE_REGISTRY.get(self._sensor_type, False) self._should_poll = POLLING_REGISTRY.get(self._sensor_type, False) self._channel = self.cluster_channels.get( - CHANNEL_REGISTRY.get(self._sensor_type, ATTRIBUTE_CHANNEL) + CHANNEL_REGISTRY.get(self._sensor_type, CHANNEL_ATTRIBUTE) ) self._device_class = DEVICE_CLASS_REGISTRY.get(self._sensor_type, None) self.state_attr_provider = DEVICE_STATE_ATTR_PROVIDER_REGISTRY.get( diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 853a50993ab..f2c00809d59 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -2,16 +2,18 @@ import logging from zigpy.zcl.foundation import Status + from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import STATE_ON from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .core.const import ( DATA_ZHA, DATA_ZHA_DISPATCHERS, - ZHA_DISCOVERY_NEW, - ON_OFF_CHANNEL, + CHANNEL_ON_OFF, SIGNAL_ATTR_UPDATED, + ZHA_DISCOVERY_NEW, ) from .entity import ZhaEntity @@ -63,7 +65,7 @@ class Switch(ZhaEntity, SwitchDevice): def __init__(self, **kwargs): """Initialize the ZHA switch.""" super().__init__(**kwargs) - self._on_off_channel = self.cluster_channels.get(ON_OFF_CHANNEL) + self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) @property def is_on(self) -> bool: diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index 0eb5bb4f230..5bf891b132e 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -1,15 +1,15 @@ """Test ZHA API.""" import pytest from homeassistant.components.switch import DOMAIN -from homeassistant.components.zha.api import async_load_api, ATTR_IEEE, TYPE, ID +from homeassistant.components.zha.api import async_load_api, TYPE, ID from homeassistant.components.zha.core.const import ( ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, - IN, - IEEE, - MODEL, - NAME, - QUIRK_APPLIED, + CLUSTER_TYPE_IN, + ATTR_IEEE, + ATTR_MODEL, + ATTR_NAME, + ATTR_QUIRK_APPLIED, ATTR_MANUFACTURER, ATTR_ENDPOINT_ID, ) @@ -49,14 +49,14 @@ async def test_device_clusters(hass, config_entry, zha_gateway, zha_client): cluster_infos = sorted(msg["result"], key=lambda k: k[ID]) cluster_info = cluster_infos[0] - assert cluster_info[TYPE] == IN + assert cluster_info[TYPE] == CLUSTER_TYPE_IN assert cluster_info[ID] == 0 - assert cluster_info[NAME] == "Basic" + assert cluster_info[ATTR_NAME] == "Basic" cluster_info = cluster_infos[1] - assert cluster_info[TYPE] == IN + assert cluster_info[TYPE] == CLUSTER_TYPE_IN assert cluster_info[ID] == 6 - assert cluster_info[NAME] == "OnOff" + assert cluster_info[ATTR_NAME] == "OnOff" async def test_device_cluster_attributes(hass, config_entry, zha_gateway, zha_client): @@ -68,7 +68,7 @@ async def test_device_cluster_attributes(hass, config_entry, zha_gateway, zha_cl ATTR_ENDPOINT_ID: 1, ATTR_IEEE: "00:0d:6f:00:0a:90:69:e7", ATTR_CLUSTER_ID: 6, - ATTR_CLUSTER_TYPE: IN, + ATTR_CLUSTER_TYPE: CLUSTER_TYPE_IN, } ) @@ -79,7 +79,7 @@ async def test_device_cluster_attributes(hass, config_entry, zha_gateway, zha_cl for attribute in attributes: assert attribute[ID] is not None - assert attribute[NAME] is not None + assert attribute[ATTR_NAME] is not None async def test_device_cluster_commands(hass, config_entry, zha_gateway, zha_client): @@ -91,7 +91,7 @@ async def test_device_cluster_commands(hass, config_entry, zha_gateway, zha_clie ATTR_ENDPOINT_ID: 1, ATTR_IEEE: "00:0d:6f:00:0a:90:69:e7", ATTR_CLUSTER_ID: 6, - ATTR_CLUSTER_TYPE: IN, + ATTR_CLUSTER_TYPE: CLUSTER_TYPE_IN, } ) @@ -102,7 +102,7 @@ async def test_device_cluster_commands(hass, config_entry, zha_gateway, zha_clie for command in commands: assert command[ID] is not None - assert command[NAME] is not None + assert command[ATTR_NAME] is not None assert command[TYPE] is not None @@ -116,13 +116,13 @@ async def test_list_devices(hass, config_entry, zha_gateway, zha_client): assert len(devices) == 1 for device in devices: - assert device[IEEE] is not None + assert device[ATTR_IEEE] is not None assert device[ATTR_MANUFACTURER] is not None - assert device[MODEL] is not None - assert device[NAME] is not None - assert device[QUIRK_APPLIED] is not None + assert device[ATTR_MODEL] is not None + assert device[ATTR_NAME] is not None + assert device[ATTR_QUIRK_APPLIED] is not None assert device["entities"] is not None for entity_reference in device["entities"]: - assert entity_reference[NAME] is not None + assert entity_reference[ATTR_NAME] is not None assert entity_reference["entity_id"] is not None From 10adae4b3b9409c4a1594ce78abae0ec7d81d555 Mon Sep 17 00:00:00 2001 From: johnnychicago Date: Fri, 2 Aug 2019 14:07:33 +0200 Subject: [PATCH 028/273] Add ELECTRICITY_IMPORTED_TOTAL obis to dsmr (#25655) --- homeassistant/components/dsmr/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 5aeb88e6399..82a81118dbd 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -63,6 +63,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ["Power Consumption", obis_ref.CURRENT_ELECTRICITY_USAGE], ["Power Production", obis_ref.CURRENT_ELECTRICITY_DELIVERY], ["Power Tariff", obis_ref.ELECTRICITY_ACTIVE_TARIFF], + ["Power Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL], ["Power Consumption (low)", obis_ref.ELECTRICITY_USED_TARIFF_1], ["Power Consumption (normal)", obis_ref.ELECTRICITY_USED_TARIFF_2], ["Power Production (low)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_1], From 0c561aec4f617f89edd5c1e6364bb6d1a62d42bd Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 2 Aug 2019 10:37:21 -0400 Subject: [PATCH 029/273] isort ZHA imports. (#25660) --- homeassistant/components/zha/__init__.py | 2 +- homeassistant/components/zha/api.py | 10 ++--- homeassistant/components/zha/binary_sensor.py | 6 +-- .../components/zha/core/channels/__init__.py | 2 +- homeassistant/components/zha/core/device.py | 18 ++++----- homeassistant/components/zha/core/gateway.py | 38 +++++++++---------- homeassistant/components/zha/core/helpers.py | 2 +- .../components/zha/core/registries.py | 16 ++++---- .../components/zha/device_tracker.py | 2 +- homeassistant/components/zha/entity.py | 4 +- homeassistant/components/zha/fan.py | 2 +- homeassistant/components/zha/light.py | 4 +- homeassistant/components/zha/lock.py | 2 +- homeassistant/components/zha/sensor.py | 8 ++-- homeassistant/components/zha/strings.json | 2 +- homeassistant/components/zha/switch.py | 2 +- 16 files changed, 60 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index b0d1ec42ec7..d71362ac1ac 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -17,6 +17,7 @@ from .core.const import ( CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG, + CONF_ENABLE_QUIRKS, CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, @@ -26,7 +27,6 @@ from .core.const import ( DEFAULT_BAUDRATE, DEFAULT_RADIO_TYPE, DOMAIN, - CONF_ENABLE_QUIRKS, RadioType, ) from .core.registries import establish_device_mappings diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 9b482dfb4f5..95fea9b5e71 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -20,17 +20,17 @@ from .core.const import ( ATTR_COMMAND_TYPE, ATTR_ENDPOINT_ID, ATTR_MANUFACTURER, + ATTR_NAME, ATTR_VALUE, + CLUSTER_COMMAND_SERVER, CLUSTER_COMMANDS_CLIENT, + CLUSTER_COMMANDS_SERVER, + CLUSTER_TYPE_IN, + CLUSTER_TYPE_OUT, DATA_ZHA, DATA_ZHA_GATEWAY, DOMAIN, - CLUSTER_TYPE_IN, MFG_CLUSTER_ID_START, - ATTR_NAME, - CLUSTER_TYPE_OUT, - CLUSTER_COMMAND_SERVER, - CLUSTER_COMMANDS_SERVER, ) from .core.helpers import async_is_bindable_target, convert_ieee, get_matched_clusters diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 082cd4542e7..24c2b92e739 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -18,19 +18,19 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( - SENSOR_ACCELERATION, CHANNEL_ATTRIBUTE, + CHANNEL_ON_OFF, + CHANNEL_ZONE, DATA_ZHA, DATA_ZHA_DISPATCHERS, + SENSOR_ACCELERATION, SENSOR_OCCUPANCY, - CHANNEL_ON_OFF, SENSOR_OPENING, SENSOR_TYPE, SIGNAL_ATTR_UPDATED, UNKNOWN, ZHA_DISCOVERY_NEW, ZONE, - CHANNEL_ZONE, ) from .entity import ZhaEntity diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 98919e46555..3a754fce2ec 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -17,9 +17,9 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from ..const import ( CHANNEL_ATTRIBUTE, CHANNEL_EVENT_RELAY, + CHANNEL_ZDO, REPORT_CONFIG_DEFAULT, SIGNAL_ATTR_UPDATED, - CHANNEL_ZDO, ) from ..helpers import ( LogMixin, diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 748a9839bb2..1fe2e1a5e6c 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -26,31 +26,31 @@ from .const import ( ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ENDPOINT_ID, - ATTR_MANUFACTURER, - ATTR_VALUE, - POWER_BATTERY_OR_UNKNOWN, - CLUSTER_COMMANDS_CLIENT, ATTR_IEEE, - CLUSTER_TYPE_IN, ATTR_LAST_SEEN, ATTR_LQI, - POWER_MAINS_POWERED, + ATTR_MANUFACTURER, ATTR_MANUFACTURER_CODE, ATTR_MODEL, ATTR_NAME, ATTR_NWK, - CLUSTER_TYPE_OUT, - CHANNEL_POWER_CONFIGURATION, ATTR_POWER_SOURCE, ATTR_QUIRK_APPLIED, ATTR_QUIRK_CLASS, ATTR_RSSI, + ATTR_VALUE, + CHANNEL_POWER_CONFIGURATION, + CHANNEL_ZDO, CLUSTER_COMMAND_SERVER, + CLUSTER_COMMANDS_CLIENT, CLUSTER_COMMANDS_SERVER, + CLUSTER_TYPE_IN, + CLUSTER_TYPE_OUT, + POWER_BATTERY_OR_UNKNOWN, + POWER_MAINS_POWERED, SIGNAL_AVAILABLE, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, - CHANNEL_ZDO, ) from .helpers import LogMixin diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 1e6367898d5..bf8eadb8f53 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -22,45 +22,45 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from ..api import async_get_device_info from .const import ( - DEBUG_RELAY_LOGGERS, + ATTR_IEEE, ATTR_MANUFACTURER, - DEBUG_COMP_BELLOWS, + ATTR_MODEL, + ATTR_NWK, + ATTR_SIGNATURE, + ATTR_TYPE, CONF_BAUDRATE, CONF_DATABASE, CONF_RADIO_TYPE, CONF_USB_PATH, CONTROLLER, - DEBUG_LEVEL_CURRENT, DATA_ZHA, DATA_ZHA_BRIDGE_ID, DATA_ZHA_GATEWAY, + DEBUG_COMP_BELLOWS, + DEBUG_COMP_ZHA, + DEBUG_COMP_ZIGPY, + DEBUG_COMP_ZIGPY_DECONZ, + DEBUG_COMP_ZIGPY_XBEE, + DEBUG_LEVEL_CURRENT, + DEBUG_LEVEL_ORIGINAL, DEBUG_LEVELS, + DEBUG_RELAY_LOGGERS, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME, + DOMAIN, + SIGNAL_REMOVE, + UNKNOWN_MANUFACTURER, + UNKNOWN_MODEL, + ZHA_GW_MSG, ZHA_GW_MSG_DEVICE_FULL_INIT, ZHA_GW_MSG_DEVICE_INFO, ZHA_GW_MSG_DEVICE_JOINED, ZHA_GW_MSG_DEVICE_REMOVED, - DOMAIN, - ATTR_IEEE, ZHA_GW_MSG_LOG_ENTRY, ZHA_GW_MSG_LOG_OUTPUT, - ATTR_MODEL, - ATTR_NWK, - DEBUG_LEVEL_ORIGINAL, + ZHA_GW_MSG_RAW_INIT, ZHA_GW_RADIO, ZHA_GW_RADIO_DESCRIPTION, - ZHA_GW_MSG_RAW_INIT, - SIGNAL_REMOVE, - ATTR_SIGNATURE, - ATTR_TYPE, - UNKNOWN_MANUFACTURER, - UNKNOWN_MODEL, - DEBUG_COMP_ZHA, - ZHA_GW_MSG, - DEBUG_COMP_ZIGPY, - DEBUG_COMP_ZIGPY_DECONZ, - DEBUG_COMP_ZIGPY_XBEE, ) from .device import DeviceStatus, ZHADevice from .discovery import async_dispatch_discovery_info, async_process_endpoint diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 23ebd9381bb..fb35ad35b98 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -12,9 +12,9 @@ import logging from homeassistant.core import callback from .const import ( - DEFAULT_BAUDRATE, CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, + DEFAULT_BAUDRATE, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MIN_INT, REPORT_CONFIG_RPT_CHANGE, diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 27e79749838..d4b198e394e 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -14,9 +14,15 @@ from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH from .const import ( + CONTROLLER, + REPORT_CONFIG_ASAP, + REPORT_CONFIG_DEFAULT, + REPORT_CONFIG_IMMEDIATE, + REPORT_CONFIG_MAX_INT, + REPORT_CONFIG_MIN_INT, + REPORT_CONFIG_OP, SENSOR_ACCELERATION, SENSOR_BATTERY, - CONTROLLER, SENSOR_ELECTRICAL_MEASUREMENT, SENSOR_HUMIDITY, SENSOR_ILLUMINANCE, @@ -24,15 +30,9 @@ from .const import ( SENSOR_OCCUPANCY, SENSOR_OPENING, SENSOR_PRESSURE, + SENSOR_TEMPERATURE, ZHA_GW_RADIO, ZHA_GW_RADIO_DESCRIPTION, - REPORT_CONFIG_ASAP, - REPORT_CONFIG_DEFAULT, - REPORT_CONFIG_IMMEDIATE, - REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_MIN_INT, - REPORT_CONFIG_OP, - SENSOR_TEMPERATURE, ZONE, RadioType, ) diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index 5d05e980dab..60a1f6c3c40 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -8,9 +8,9 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( + CHANNEL_POWER_CONFIGURATION, DATA_ZHA, DATA_ZHA_DISPATCHERS, - CHANNEL_POWER_CONFIGURATION, SIGNAL_ATTR_UPDATED, ZHA_DISCOVERY_NEW, ) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index dd5364b09e0..694f7b25695 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -13,11 +13,11 @@ from homeassistant.util import slugify from .core.const import ( ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, DATA_ZHA, DATA_ZHA_BRIDGE_ID, DOMAIN, - ATTR_MODEL, - ATTR_NAME, SIGNAL_REMOVE, ) from .core.helpers import LogMixin diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index e20d147006a..1f119ef6657 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -14,9 +14,9 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( + CHANNEL_FAN, DATA_ZHA, DATA_ZHA_DISPATCHERS, - CHANNEL_FAN, SIGNAL_ATTR_UPDATED, ZHA_DISCOVERY_NEW, ) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 80d6af81605..379f69febbb 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -13,10 +13,10 @@ import homeassistant.util.color as color_util from .core.const import ( CHANNEL_COLOR, - DATA_ZHA, - DATA_ZHA_DISPATCHERS, CHANNEL_LEVEL, CHANNEL_ON_OFF, + DATA_ZHA, + DATA_ZHA_DISPATCHERS, SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL, ZHA_DISCOVERY_NEW, diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index a7b2d393012..afc4618343c 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -13,9 +13,9 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( + CHANNEL_DOORLOCK, DATA_ZHA, DATA_ZHA_DISPATCHERS, - CHANNEL_DOORLOCK, SIGNAL_ATTR_UPDATED, ZHA_DISCOVERY_NEW, ) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index df8e2add7dd..e38acebb22c 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -17,21 +17,21 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( CHANNEL_ATTRIBUTE, - SENSOR_BATTERY, + CHANNEL_ELECTRICAL_MEASUREMENT, + CHANNEL_POWER_CONFIGURATION, DATA_ZHA, DATA_ZHA_DISPATCHERS, + SENSOR_BATTERY, SENSOR_ELECTRICAL_MEASUREMENT, - CHANNEL_ELECTRICAL_MEASUREMENT, SENSOR_GENERIC, SENSOR_HUMIDITY, SENSOR_ILLUMINANCE, SENSOR_METERING, - CHANNEL_POWER_CONFIGURATION, SENSOR_PRESSURE, + SENSOR_TEMPERATURE, SENSOR_TYPE, SIGNAL_ATTR_UPDATED, SIGNAL_STATE_ATTR, - SENSOR_TEMPERATURE, UNKNOWN, ZHA_DISCOVERY_NEW, ) diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index b6d7948c0b3..aacfd2a0d96 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -18,4 +18,4 @@ "cannot_connect": "Unable to connect to ZHA device." } } -} \ No newline at end of file +} diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index f2c00809d59..bfe816d614a 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -9,9 +9,9 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( + CHANNEL_ON_OFF, DATA_ZHA, DATA_ZHA_DISPATCHERS, - CHANNEL_ON_OFF, SIGNAL_ATTR_UPDATED, ZHA_DISCOVERY_NEW, ) From a2f9a5287d061f483d7ca1b2fdc7ed2e35cb8fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 2 Aug 2019 17:59:47 +0300 Subject: [PATCH 030/273] Azure mypy related tweaks (#25663) * Revert "Add some debugging to azure mypy job (#25632)" This reverts commit 767b8e9f25affbebb98d65f57c0d78b532a951ba. No longer needed. * Install setup.py dependencies for mypy in Azure For better coverage, and to match what tox does. --- azure-pipelines-ci.yml | 5 +---- homeassistant/util/ruamel_yaml.py | 3 ++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index eef4731c4ac..1e3d914aaf0 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -201,19 +201,16 @@ stages: container: $[ variables['PythonMain'] ] steps: - script: | - set -x python -m venv venv . venv/bin/activate + pip install -e . pip install -r requirements_test.txt -c homeassistant/package_constraints.txt displayName: 'Setup Env' - script: | - set -x TYPING_FILES=$(cat mypyrc) echo -e "Run mypy on: \n$TYPING_FILES" . venv/bin/activate - python --version - mypy --version mypy $TYPING_FILES displayName: 'Run mypy' diff --git a/homeassistant/util/ruamel_yaml.py b/homeassistant/util/ruamel_yaml.py index 04a98a84f03..931ad7adb91 100644 --- a/homeassistant/util/ruamel_yaml.py +++ b/homeassistant/util/ruamel_yaml.py @@ -88,7 +88,8 @@ def load_yaml(fname: str, round_trip: bool = False) -> JSON_TYPE: """Load a YAML file.""" if round_trip: yaml = YAML(typ="rt") - yaml.preserve_quotes = True + # type ignore: https://bitbucket.org/ruamel/yaml/pull-requests/42 + yaml.preserve_quotes = True # type: ignore else: if ExtSafeConstructor.name is None: ExtSafeConstructor.name = fname From 2f7c57b257d0678b00846aeba2abb90851e96a70 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 2 Aug 2019 17:00:22 +0200 Subject: [PATCH 031/273] Revert flux_led to 0.89 (#25653) * Revert Black * Revert "Introduce support for color temperature (#25503)" This reverts commit e1d884a4841f853bb4247a76fca417c0b4f4e7c1. * Revert "Fix flux_led only-white controllers (#22210)" This reverts commit 48138189b3c24261fe62a78b6ec854c761d5ce63. * Revert "Fix MagicHome LEDs with flux_led component (#20733)" This reverts commit 1444a684e02fab99648f6e5daea9f28b6cf45c10. * Re-Black * Use mode detection for scanned bulbs --- homeassistant/components/flux_led/light.py | 138 ++++++++------------- 1 file changed, 55 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index cef0387111a..23fdb38aa05 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -2,8 +2,6 @@ import logging import socket import random -from asyncio import sleep -from functools import partial import voluptuous as vol @@ -13,14 +11,12 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_EFFECT, ATTR_WHITE_VALUE, - ATTR_COLOR_TEMP, EFFECT_COLORLOOP, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, SUPPORT_COLOR, SUPPORT_WHITE_VALUE, - SUPPORT_COLOR_TEMP, Light, PLATFORM_SCHEMA, ) @@ -38,9 +34,7 @@ ATTR_MODE = "mode" DOMAIN = "flux_led" -SUPPORT_FLUX_LED = ( - SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_COLOR | SUPPORT_COLOR_TEMP -) +SUPPORT_FLUX_LED = SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_COLOR MODE_RGB = "rgb" MODE_RGBW = "rgbw" @@ -49,11 +43,6 @@ MODE_RGBW = "rgbw" # RGB value is ignored when this mode is specified. MODE_WHITE = "w" -# Constant color temp values for 2 flux_led special modes -# Warm-white and Cool-white. Details on #23704 -COLOR_TEMP_WARM_WHITE = 333 -COLOR_TEMP_COOL_WHITE = 250 - # List of supported effects which aren't already declared in LIGHT EFFECT_RED_FADE = "red_fade" EFFECT_GREEN_FADE = "green_fade" @@ -196,8 +185,6 @@ class FluxLight(Light): self._custom_effect = device[CONF_CUSTOM_EFFECT] self._bulb = None self._error_reported = False - self._color = (0, 0, 100) - self._white_value = 0 def _connect(self): """Connect to Flux light.""" @@ -238,14 +225,14 @@ class FluxLight(Light): def brightness(self): """Return the brightness of this light between 0..255.""" if self._mode == MODE_WHITE: - return self._white_value + return self.white_value - return int(self._color[2] / 100 * 255) + return self._bulb.brightness @property def hs_color(self): """Return the color property.""" - return self._color[0:2] + return color_util.color_RGB_to_hs(*self._bulb.getRgb()) @property def supported_features(self): @@ -261,7 +248,7 @@ class FluxLight(Light): @property def white_value(self): """Return the white value of this light between 0..255.""" - return self._white_value + return self._bulb.getRgbw()[3] @property def effect_list(self): @@ -282,85 +269,75 @@ class FluxLight(Light): for effect, code in EFFECT_MAP.items(): if current_mode == code: return effect + return None - async def async_turn_on(self, **kwargs): - """Turn the specified or all lights on and wait for state.""" - await self.hass.async_add_executor_job(partial(self._turn_on, **kwargs)) - # The bulb needs a bit to tell its new values, - # so we wait 1 second before updating - await sleep(1) - - def _turn_on(self, **kwargs): + def turn_on(self, **kwargs): """Turn the specified or all lights on.""" - self._bulb.turnOn() + if not self.is_on: + self._bulb.turnOn() hs_color = kwargs.get(ATTR_HS_COLOR) + + if hs_color: + rgb = color_util.color_hs_to_RGB(*hs_color) + else: + rgb = None + brightness = kwargs.get(ATTR_BRIGHTNESS) effect = kwargs.get(ATTR_EFFECT) white = kwargs.get(ATTR_WHITE_VALUE) - color_temp = kwargs.get(ATTR_COLOR_TEMP) - if all( - item is None for item in [hs_color, brightness, effect, white, color_temp] - ): + # Show warning if effect set with rgb, brightness, or white level + if effect and (brightness or white or rgb): + _LOGGER.warning( + "RGB, brightness and white level are ignored when" + " an effect is specified for a flux bulb" + ) + + # Random color effect + if effect == EFFECT_RANDOM: + self._bulb.setRgb( + random.randint(0, 255), random.randint(0, 255), random.randint(0, 255) + ) return - # handle W only mode (use brightness instead of white value) - if self._mode == MODE_WHITE: - if brightness is not None: - self._bulb.setWarmWhite255(brightness) - return - - # handle effects - if effect is not None: - # Random color effect - if effect == EFFECT_RANDOM: - self._bulb.setRgb( - random.randint(0, 255), - random.randint(0, 255), - random.randint(0, 255), + if effect == EFFECT_CUSTOM: + if self._custom_effect: + self._bulb.setCustomPattern( + self._custom_effect[CONF_COLORS], + self._custom_effect[CONF_SPEED_PCT], + self._custom_effect[CONF_TRANSITION], ) - elif effect == EFFECT_CUSTOM: - if self._custom_effect: - self._bulb.setCustomPattern( - self._custom_effect[CONF_COLORS], - self._custom_effect[CONF_SPEED_PCT], - self._custom_effect[CONF_TRANSITION], - ) - # Effect selection - elif effect in EFFECT_MAP: - self._bulb.setPresetPattern(EFFECT_MAP[effect], 50) return - # handle special modes - if color_temp is not None: - if brightness is None: - brightness = self.brightness - if color_temp == COLOR_TEMP_WARM_WHITE: - self._bulb.setRgbw(w=brightness) - elif color_temp == COLOR_TEMP_COOL_WHITE: - self._bulb.setRgbw(w2=brightness) - else: - self._bulb.setRgbw(*color_util.color_temperature_to_rgb(color_temp)) + # Effect selection + if effect in EFFECT_MAP: + self._bulb.setPresetPattern(EFFECT_MAP[effect], 50) return # Preserve current brightness on color/white level change - if hs_color is not None: - if brightness is None: - brightness = self.brightness - color = (hs_color[0], hs_color[1], brightness / 255 * 100) - elif brightness is not None: - color = (self._color[0], self._color[1], brightness / 255 * 100) + if brightness is None: + brightness = self.brightness + + # Preserve color on brightness/white level change + if rgb is None: + rgb = self._bulb.getRgb() + + if white is None and self._mode == MODE_RGBW: + white = self.white_value + + # handle W only mode (use brightness instead of white value) + if self._mode == MODE_WHITE: + self._bulb.setRgbw(0, 0, 0, w=brightness) + # handle RGBW mode - if self._mode == MODE_RGBW: - if white is None: - self._bulb.setRgbw(*color_util.color_hsv_to_RGB(*color)) - else: - self._bulb.setRgbw(w=white) + elif self._mode == MODE_RGBW: + self._bulb.setRgbw(*tuple(rgb), w=white, brightness=brightness) + # handle RGB mode else: - self._bulb.setRgb(*color_util.color_hsv_to_RGB(*color)) + self._bulb.setRgb(*tuple(rgb), brightness=brightness) def turn_off(self, **kwargs): """Turn the specified or all lights off.""" @@ -380,10 +357,5 @@ class FluxLight(Light): ) self._error_reported = True return + self._bulb.update_state(retry=2) - if self._mode != MODE_WHITE and self._bulb.getRgb() != (0, 0, 0): - color = self._bulb.getRgbw() - self._color = color_util.color_RGB_to_hsv(*color[0:3]) - self._white_value = color[3] - elif self._mode == MODE_WHITE: - self._white_value = self._bulb.getRgbw()[3] From a54ade11897589ab83c8f4bb6dc8a25a77c76a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 3 Aug 2019 00:20:07 +0300 Subject: [PATCH 032/273] Spelling fixes (#25666) --- homeassistant/components/alexa/const.py | 2 +- homeassistant/components/alexa/messages.py | 2 +- homeassistant/components/amazon_polly/tts.py | 2 +- .../components/arcam_fmj/media_player.py | 2 +- .../components/auth/mfa_setup_flow.py | 2 +- homeassistant/components/climate/const.py | 2 +- homeassistant/components/delijn/sensor.py | 4 ++-- homeassistant/components/denon/media_player.py | 2 +- .../components/emulated_hue/__init__.py | 2 +- homeassistant/components/fibaro/climate.py | 2 +- homeassistant/components/google_cloud/tts.py | 2 +- .../homekit_controller/config_flow.py | 2 +- homeassistant/components/homematic/climate.py | 6 +++--- .../components/homematicip_cloud/sensor.py | 18 +++++++++--------- .../components/huawei_lte/manifest.json | 2 +- .../components/hue/.translations/cy.json | 4 ++-- .../components/launch_library/sensor.py | 2 +- homeassistant/components/linky/sensor.py | 2 +- .../components/meteo_france/__init__.py | 2 +- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/mqtt/cover.py | 2 +- homeassistant/components/netgear_lte/notify.py | 2 +- .../components/point/alarm_control_panel.py | 2 +- .../components/point/binary_sensor.py | 2 +- homeassistant/components/rachio/switch.py | 4 ++-- homeassistant/components/raspyrfm/switch.py | 2 +- homeassistant/components/rejseplanen/sensor.py | 2 +- homeassistant/components/ruter/sensor.py | 2 +- homeassistant/components/ssdp/__init__.py | 2 +- homeassistant/components/stream/core.py | 2 +- homeassistant/components/stream/worker.py | 4 ++-- .../components/switcher_kis/__init__.py | 2 +- .../components/system_log/__init__.py | 2 +- homeassistant/components/tod/binary_sensor.py | 2 +- homeassistant/components/vesync/switch.py | 2 +- homeassistant/components/wirelesstag/sensor.py | 2 +- homeassistant/components/zhong_hong/climate.py | 2 +- homeassistant/helpers/config_validation.py | 2 +- 38 files changed, 52 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 282df1756d7..83c7da41c16 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -47,7 +47,7 @@ CONF_DISPLAY_CATEGORIES = "display_categories" API_TEMP_UNITS = {TEMP_FAHRENHEIT: "FAHRENHEIT", TEMP_CELSIUS: "CELSIUS"} # Needs to be ordered dict for `async_api_set_thermostat_mode` which does a -# reverse mapping of this dict and we want to map the first occurrance of OFF +# reverse mapping of this dict and we want to map the first occurrence of OFF # back to HA state. API_THERMOSTAT_MODES = OrderedDict( [ diff --git a/homeassistant/components/alexa/messages.py b/homeassistant/components/alexa/messages.py index 2b7d15ac841..3195656ed09 100644 --- a/homeassistant/components/alexa/messages.py +++ b/homeassistant/components/alexa/messages.py @@ -163,7 +163,7 @@ class AlexaResponse: The Alexa response includes a list of properties which provides feedback on how states have changed. For example if a user asks, - "Alexa, set theromstat to 20 degrees", the API expects a response with + "Alexa, set thermostat to 20 degrees", the API expects a response with the new value of the property, and Alexa will respond to the user "Thermostat set to 20 degrees". diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index f27b0428072..c7098867ee8 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -45,7 +45,7 @@ SUPPORTED_VOICES = [ "Ruben", "Lotte", # Dutch "Russell", - "Nicole", # English Austrailian + "Nicole", # English Australian "Brian", "Amy", "Emma", # English diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 5cbfe2dd482..971abc3e26d 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -124,7 +124,7 @@ class ArcamFmj(MediaPlayerDevice): return support async def async_added_to_hass(self): - """Once registed add listener for events.""" + """Once registered, add listener for events.""" await self._state.start() @callback diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index 89c3e87f78a..c18bc276a44 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -31,7 +31,7 @@ async def async_setup(hass): """Init mfa setup flow manager.""" async def _async_create_setup_flow(handler, context, data): - """Create a setup flow. hanlder is a mfa module.""" + """Create a setup flow. handler is a mfa module.""" mfa_module = hass.auth.get_auth_mfa_module(handler) if mfa_module is None: raise ValueError("Mfa module {} is not found".format(handler)) diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py index ba13c03babd..4012aa8be1b 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -108,7 +108,7 @@ ATTR_TARGET_TEMP_STEP = "target_temp_step" DEFAULT_MIN_TEMP = 7 DEFAULT_MAX_TEMP = 35 -DEFAULT_MIN_HUMITIDY = 30 +DEFAULT_MIN_HUMIDITY = 30 DEFAULT_MAX_HUMIDITY = 99 DOMAIN = "climate" diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index e704ef6556f..2cd238d0787 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -50,7 +50,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) await line.get_passages() if line.passages is None: - _LOGGER.warning("No data recieved from De Lijn") + _LOGGER.warning("No data received from De Lijn") return sensors.append(DeLijnPublicTransportSensor(line, name)) @@ -71,7 +71,7 @@ class DeLijnPublicTransportSensor(Entity): """Get the latest data from the De Lijn API.""" await self.line.get_passages() if self.line.passages is None: - _LOGGER.warning("No data recieved from De Lijn") + _LOGGER.warning("No data received from De Lijn") return try: first = self.line.passages[0] diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index d9ff52b47d0..7bed8423e8f 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -51,7 +51,7 @@ NORMAL_INPUTS = { "Dvd": "DVD", "Blue ray": "BD", "TV": "TV", - "Satelite / Cable": "SAT/CBL", + "Satellite / Cable": "SAT/CBL", "Game": "GAME", "Game2": "GAME2", "Video Aux": "V.AUX", diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index b9e3ecaf093..791085b46f3 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -1,4 +1,4 @@ -"""Support for local control of entities by emulating a Phillips Hue bridge.""" +"""Support for local control of entities by emulating a Philips Hue bridge.""" import logging from aiohttp import web diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index 4670a3f0c62..ed399fac209 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -50,7 +50,7 @@ HA_FANMODES = {v: k for k, v in FANMODES.items()} # SDS13781-10 Z-Wave Application Command Class Specification 2019-01-04 # Table 130, Thermostat Mode Set version 3::Mode encoding. -# 4 AUXILARY +# 4 AUXILIARY OPMODES_PRESET = { 5: PRESET_RESUME, 7: PRESET_FURNACE, diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 54c5806101b..cf064b3bfa7 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -256,6 +256,6 @@ class GoogleCloudTTSProvider(Provider): except asyncio.TimeoutError as ex: _LOGGER.error("Timeout for Google Cloud TTS call: %s", ex) except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception("Error occured during Google Cloud TTS call: %s", ex) + _LOGGER.exception("Error occurred during Google Cloud TTS call: %s", ex) return None, None diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 00214282123..e5337295a70 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -262,7 +262,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): # PairVerify M4 - Ed25519 signature verification failed errors["pairing_code"] = "authentication_error" except homekit.UnknownError: - # An error occured on the device whilst performing this + # An error occurred on the device whilst performing this # operation. errors["pairing_code"] = "unknown_error" except homekit.MaxPeersError: diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 008055b649a..1a2f642f91c 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -69,7 +69,7 @@ class HMThermostat(HMDevice, ClimateDevice): if self.target_temperature <= self._hmdevice.OFF_VALUE + 0.5: return HVAC_MODE_OFF if "MANU_MODE" in self._hmdevice.ACTIONNODE: - if self._hm_controll_mode == self._hmdevice.MANU_MODE: + if self._hm_control_mode == self._hmdevice.MANU_MODE: return HVAC_MODE_HEAT return HVAC_MODE_AUTO @@ -95,7 +95,7 @@ class HMThermostat(HMDevice, ClimateDevice): return "boost" # Get the name of the mode - mode = HM_ATTRIBUTE_SUPPORT[HM_CONTROL_MODE][1][self._hm_controll_mode] + mode = HM_ATTRIBUTE_SUPPORT[HM_CONTROL_MODE][1][self._hm_control_mode] mode = mode.lower() # Filter HVAC states @@ -173,7 +173,7 @@ class HMThermostat(HMDevice, ClimateDevice): return 0.5 @property - def _hm_controll_mode(self): + def _hm_control_mode(self): """Return Control mode.""" if HMIP_CONTROL_MODE in self._data: return self._data[HMIP_CONTROL_MODE] diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index b3cbf4627d4..add03c6b644 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -144,7 +144,7 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): class HomematicipHeatingThermostat(HomematicipGenericDevice): - """Represenation of a HomematicIP heating thermostat device.""" + """Representation of a HomematicIP heating thermostat device.""" def __init__(self, home: AsyncHome, device) -> None: """Initialize heating thermostat device.""" @@ -173,7 +173,7 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice): class HomematicipHumiditySensor(HomematicipGenericDevice): - """Represenation of a HomematicIP Cloud humidity device.""" + """Representation of a HomematicIP Cloud humidity device.""" def __init__(self, home: AsyncHome, device) -> None: """Initialize the thermometer device.""" @@ -233,7 +233,7 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice): class HomematicipIlluminanceSensor(HomematicipGenericDevice): - """Represenation of a HomematicIP Illuminance device.""" + """Representation of a HomematicIP Illuminance device.""" def __init__(self, home: AsyncHome, device) -> None: """Initialize the device.""" @@ -259,7 +259,7 @@ class HomematicipIlluminanceSensor(HomematicipGenericDevice): class HomematicipPowerSensor(HomematicipGenericDevice): - """Represenation of a HomematicIP power measuring device.""" + """Representation of a HomematicIP power measuring device.""" def __init__(self, home: AsyncHome, device) -> None: """Initialize the device.""" @@ -272,7 +272,7 @@ class HomematicipPowerSensor(HomematicipGenericDevice): @property def state(self) -> float: - """Represenation of the HomematicIP power comsumption value.""" + """Representation of the HomematicIP power comsumption value.""" return self._device.currentPowerConsumption @property @@ -282,7 +282,7 @@ class HomematicipPowerSensor(HomematicipGenericDevice): class HomematicipWindspeedSensor(HomematicipGenericDevice): - """Represenation of a HomematicIP wind speed sensor.""" + """Representation of a HomematicIP wind speed sensor.""" def __init__(self, home: AsyncHome, device) -> None: """Initialize the device.""" @@ -290,7 +290,7 @@ class HomematicipWindspeedSensor(HomematicipGenericDevice): @property def state(self) -> float: - """Represenation of the HomematicIP wind speed value.""" + """Representation of the HomematicIP wind speed value.""" return self._device.windSpeed @property @@ -313,7 +313,7 @@ class HomematicipWindspeedSensor(HomematicipGenericDevice): class HomematicipTodayRainSensor(HomematicipGenericDevice): - """Represenation of a HomematicIP rain counter of a day sensor.""" + """Representation of a HomematicIP rain counter of a day sensor.""" def __init__(self, home: AsyncHome, device) -> None: """Initialize the device.""" @@ -321,7 +321,7 @@ class HomematicipTodayRainSensor(HomematicipGenericDevice): @property def state(self) -> float: - """Represenation of the HomematicIP todays rain value.""" + """Representation of the HomematicIP todays rain value.""" return round(self._device.todayRainCounter, 2) @property diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index f31ff74c055..85077511768 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -1,6 +1,6 @@ { "domain": "huawei_lte", - "name": "Huawei lte", + "name": "Huawei LTE", "documentation": "https://www.home-assistant.io/components/huawei_lte", "requirements": [ "getmac==0.8.1", diff --git a/homeassistant/components/hue/.translations/cy.json b/homeassistant/components/hue/.translations/cy.json index f5476f73edb..282dd371499 100644 --- a/homeassistant/components/hue/.translations/cy.json +++ b/homeassistant/components/hue/.translations/cy.json @@ -24,6 +24,6 @@ "title": "Hwb cyswllt" } }, - "title": "Pont Phillips Hue" + "title": "Pont Philips Hue" } -} \ No newline at end of file +} diff --git a/homeassistant/components/launch_library/sensor.py b/homeassistant/components/launch_library/sensor.py index 71246872663..14a75704312 100644 --- a/homeassistant/components/launch_library/sensor.py +++ b/homeassistant/components/launch_library/sensor.py @@ -49,7 +49,7 @@ class LaunchLibrarySensor(Entity): """Get the latest data.""" await self.launches.get_launches() if self.launches.launches is None: - _LOGGER.error("No data recieved") + _LOGGER.error("No data received") return try: data = self.launches.launches[0] diff --git a/homeassistant/components/linky/sensor.py b/homeassistant/components/linky/sensor.py index aa5e6516e2e..98aca67d8ea 100644 --- a/homeassistant/components/linky/sensor.py +++ b/homeassistant/components/linky/sensor.py @@ -152,7 +152,7 @@ class LinkySensor(Entity): } def update(self): - """Retreive the new data for the sensor.""" + """Retrieve the new data for the sensor.""" data = self.__account.data[self._scale][self.__when] self.__consumption = data[CONSUMPTION] self.__time = data[TIME] diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index ab3ec45867b..89fcc3c98aa 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -152,7 +152,7 @@ def setup(hass, config): if CONF_MONITORED_CONDITIONS in location: monitored_conditions = location[CONF_MONITORED_CONDITIONS] - _LOGGER.debug("meteo_france sensor platfrom loaded for %s", city) + _LOGGER.debug("meteo_france sensor platform loaded for %s", city) load_platform( hass, "sensor", diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 0a93c29e8b0..af2fbac9639 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -431,7 +431,7 @@ async def async_subscribe( ) wrapped_msg_callback = msg_callback - # If we have 3 paramaters with no default value, wrap the callback + # If we have 3 parameters with no default value, wrap the callback if non_default == 3: _LOGGER.warning( "Signature of MQTT msg_callback '%s.%s' is deprecated", diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 032a176932e..66e14ca9a5a 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -102,7 +102,7 @@ TILT_FEATURES = ( def validate_options(value): """Validate options. - If set postion topic is set then get position topic is set as well. + If set position topic is set then get position topic is set as well. """ if CONF_SET_POSITION_TOPIC in value and CONF_GET_POSITION_TOPIC not in value: raise vol.Invalid( diff --git a/homeassistant/components/netgear_lte/notify.py b/homeassistant/components/netgear_lte/notify.py index 0555ab1ffe8..4f13662519d 100644 --- a/homeassistant/components/netgear_lte/notify.py +++ b/homeassistant/components/netgear_lte/notify.py @@ -1,4 +1,4 @@ -"""Suport for Netgear LTE notifications.""" +"""Support for Netgear LTE notifications.""" import logging import attr diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py index b55fb9f681d..7dc7e164788 100644 --- a/homeassistant/components/point/alarm_control_panel.py +++ b/homeassistant/components/point/alarm_control_panel.py @@ -65,7 +65,7 @@ class MinutPointAlarmControl(AlarmControlPanel): _device_id = data.get("event", {}).get("device_id") if _device_id not in self._home["devices"] or _type not in EVENT_MAP: return - _LOGGER.debug("Recieved webhook: %s", _type) + _LOGGER.debug("Received webhook: %s", _type) self._home["alarm_status"] = EVENT_MAP[_type] self._changed_by = _device_id self.async_schedule_update_ha_state() diff --git a/homeassistant/components/point/binary_sensor.py b/homeassistant/components/point/binary_sensor.py index a5704a1fc17..a08f7dbedc4 100644 --- a/homeassistant/components/point/binary_sensor.py +++ b/homeassistant/components/point/binary_sensor.py @@ -106,7 +106,7 @@ class MinutPointBinarySensor(MinutPointEntity, BinarySensorDevice): _device_id = data.get("event", {}).get("device_id") if _type not in self._events or _device_id != self.device.device_id: return - _LOGGER.debug("Recieved webhook: %s", _type) + _LOGGER.debug("Received webhook: %s", _type) if _type == self._events[0]: self._is_on = True if _type == self._events[1]: diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 04ed47f2a2a..b65e6bf6044 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -117,7 +117,7 @@ class RachioStandbySwitch(RachioSwitch): @property def unique_id(self) -> str: - """Return a unique id by combinining controller id and purpose.""" + """Return a unique id by combining controller id and purpose.""" return "{}-standby".format(self._controller.controller_id) @property @@ -182,7 +182,7 @@ class RachioZone(RachioSwitch): @property def unique_id(self) -> str: - """Return a unique id by combinining controller id and zone number.""" + """Return a unique id by combining controller id and zone number.""" return "{}-zone-{}".format(self._controller.controller_id, self.zone_id) @property diff --git a/homeassistant/components/raspyrfm/switch.py b/homeassistant/components/raspyrfm/switch.py index 1dbe4ab776d..53cb1dbdcb5 100644 --- a/homeassistant/components/raspyrfm/switch.py +++ b/homeassistant/components/raspyrfm/switch.py @@ -1,4 +1,4 @@ -"""Support for switchs that can be controlled using the RaspyRFM rc module.""" +"""Support for switches that can be controlled using the RaspyRFM rc module.""" import logging import voluptuous as vol diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py index 99cfe1067e8..3172e614166 100755 --- a/homeassistant/components/rejseplanen/sensor.py +++ b/homeassistant/components/rejseplanen/sensor.py @@ -192,7 +192,7 @@ class PublicTransportData: _LOGGER.debug("API returned error: %s", error) return except (rjpl.rjplConnectionError, rjpl.rjplHTTPError): - _LOGGER.debug("Error occured while connecting to the API") + _LOGGER.debug("Error occurred while connecting to the API") return # Filter result diff --git a/homeassistant/components/ruter/sensor.py b/homeassistant/components/ruter/sensor.py index 74e84fdec7c..ba4d7368628 100644 --- a/homeassistant/components/ruter/sensor.py +++ b/homeassistant/components/ruter/sensor.py @@ -62,7 +62,7 @@ class RuterSensor(Entity): """Get the latest data from the Ruter API.""" await self.ruter.get_departures() if self.ruter.departures is None: - _LOGGER.error("No data recieved from Ruter.") + _LOGGER.error("No data received from Ruter.") return try: data = self.ruter.departures[self._offset] diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index b5a903f9fae..a8591ac042b 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -112,7 +112,7 @@ class Scanner: return (entry, info_from_entry(entry, None), domains) return None - # Multiple entries usally share same location. Make sure + # Multiple entries usually share same location. Make sure # we fetch it only once. info_req = self._description_cache.get(xml_location) diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 426eb568f93..81335783e1a 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -114,7 +114,7 @@ class StreamOutput: @callback def put(self, segment: Segment) -> None: """Store output.""" - # Start idle timeout when we start recieving data + # Start idle timeout when we start receiving data if self._unsub is None: self._unsub = async_call_later( self._stream.hass, self.timeout, self._timeout diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 90b2f518b45..e87221304a3 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -63,7 +63,7 @@ def stream_worker(hass, stream, quit_event): sequence = 1 # Holds the generated silence that needs to be muxed into the output audio_packets = {} - # The presentation timestamp of the first video packet we recieve + # The presentation timestamp of the first video packet we receive first_pts = 0 # The decoder timestamp of the latest packet we processed last_dts = None @@ -130,7 +130,7 @@ def stream_worker(hass, stream, quit_event): # If we are attaching to a live stream that does not reset # timestamps for us, we need to do it ourselves by recording # the first presentation timestamp and subtracting it from - # subsequent packets we recieve. + # subsequent packets we receive. if (packet.pts * packet.time_base) > 1: first_pts = packet.pts packet.dts = 0 diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 1687f8eee88..9f4347d61d2 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -116,7 +116,7 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: async def async_switch_platform_discovered( platform: str, discovery_info: Optional[Dict] ) -> None: - """Use for registering services after switch platform is discoverd.""" + """Use for registering services after switch platform is discovered.""" if platform != DOMAIN: return diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index beae6ce55c0..c9bd486053e 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -116,7 +116,7 @@ class LogEntry: return frozenset([self.message, self.root_cause]) def to_dict(self): - """Convert object into dict to maintain backward compatability.""" + """Convert object into dict to maintain backward compatibility.""" return vars(self) diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index 2f8f4acb51f..6c5d8827a86 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -84,7 +84,7 @@ class TodSensor(BinarySensorDevice): @property def after(self): - """Return the timestamp for the begining of the period.""" + """Return the timestamp for the beginning of the period.""" return self._time_after @property diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py index fbbf7f4faad..5ca76a77254 100644 --- a/homeassistant/components/vesync/switch.py +++ b/homeassistant/components/vesync/switch.py @@ -46,7 +46,7 @@ def _async_setup_entities(devices, async_add_entities): dev_list.append(VeSyncLightSwitch(dev)) else: _LOGGER.warning( - "%s - Unkown device type - %s", dev.device_name, dev.device_type + "%s - Unknown device type - %s", dev.device_name, dev.device_type ) continue diff --git a/homeassistant/components/wirelesstag/sensor.py b/homeassistant/components/wirelesstag/sensor.py index df8a9c46ac7..fa72ab184e1 100644 --- a/homeassistant/components/wirelesstag/sensor.py +++ b/homeassistant/components/wirelesstag/sensor.py @@ -72,7 +72,7 @@ class WirelessTagSensor(WirelessTagBaseSensor): @property def entity_id(self): - """Overriden version.""" + """Overridden version.""" return self._entity_id @property diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index bfc730a285e..8514ec711cb 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -82,7 +82,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): async_dispatcher_connect(hass, SIGNAL_DEVICE_ADDED, startup) - # add devices after SIGNAL_DEVICE_SETTED_UP event is listend + # add devices after SIGNAL_DEVICE_SETTED_UP event is listened add_entities(devices) def stop_listen(event): diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index ece6b0753a2..b52a17b2e39 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -261,7 +261,7 @@ def icon(value): if ":" in value: return value - raise vol.Invalid('Icons should be specifed on the form "prefix:name"') + raise vol.Invalid('Icons should be specified in the form "prefix:name"') time_period_dict = vol.All( From 93dfd613aa52d825ea97f8f1819a4bccbef3e385 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 2 Aug 2019 23:51:06 +0200 Subject: [PATCH 033/273] Options to not track wired clients (#25669) --- homeassistant/components/unifi/__init__.py | 2 ++ homeassistant/components/unifi/const.py | 1 + .../components/unifi/device_tracker.py | 7 +++++++ tests/components/unifi/test_device_tracker.py | 18 ++++++++++++++++++ 4 files changed, 28 insertions(+) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index f4df139001d..4ca6f68c301 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -13,6 +13,7 @@ from .const import ( CONF_DETECTION_TIME, CONF_DONT_TRACK_CLIENTS, CONF_DONT_TRACK_DEVICES, + CONF_DONT_TRACK_WIRED_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, CONTROLLER_ID, @@ -32,6 +33,7 @@ CONTROLLER_SCHEMA = vol.Schema( ), vol.Optional(CONF_DONT_TRACK_CLIENTS): cv.boolean, vol.Optional(CONF_DONT_TRACK_DEVICES): cv.boolean, + vol.Optional(CONF_DONT_TRACK_WIRED_CLIENTS): cv.boolean, vol.Optional(CONF_DETECTION_TIME): vol.All( cv.time_period, cv.positive_timedelta ), diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index 1295849704c..b4864421cb9 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -15,6 +15,7 @@ CONF_BLOCK_CLIENT = "block_client" CONF_DETECTION_TIME = "detection_time" CONF_DONT_TRACK_CLIENTS = "dont_track_clients" CONF_DONT_TRACK_DEVICES = "dont_track_devices" +CONF_DONT_TRACK_WIRED_CLIENTS = "dont_track_wired_clients" CONF_SSID_FILTER = "ssid_filter" ATTR_MANUFACTURER = "Ubiquiti Networks" diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index ce5a1a7f608..9c645a072a5 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -30,6 +30,7 @@ from .const import ( CONF_DETECTION_TIME, CONF_DONT_TRACK_CLIENTS, CONF_DONT_TRACK_DEVICES, + CONF_DONT_TRACK_WIRED_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, CONTROLLER_ID, @@ -178,6 +179,12 @@ def update_items(controller, async_add_entities, tracked): ): continue + if ( + controller.unifi_config.get(CONF_DONT_TRACK_WIRED_CLIENTS, False) + and client.is_wired + ): + continue + tracked[client_id] = UniFiClientTracker(client, controller) new_tracked.append(tracked[client_id]) LOGGER.debug( diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index fb13bef42aa..9fca9d21a5b 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -280,3 +280,21 @@ async def test_dont_track_devices(hass, mock_controller): device_1 = hass.states.get("device_tracker.device_1") assert device_1 is None + + +async def test_dont_track_wired_clients(hass, mock_controller): + """Test dont track wired clients config works.""" + mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2]) + mock_controller.mock_device_responses.append({}) + mock_controller.unifi_config = {unifi.CONF_DONT_TRACK_WIRED_CLIENTS: True} + + await setup_controller(hass, mock_controller) + assert len(mock_controller.mock_requests) == 2 + assert len(hass.states.async_all()) == 3 + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is not None + assert client_1.state == "not_home" + + client_2 = hass.states.get("device_tracker.client_2") + assert client_2 is None From 73f55757088709214f3498b56ebb8780dc5e8217 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 3 Aug 2019 18:49:34 +0200 Subject: [PATCH 034/273] Add PRESET_AWAY to HomematicIP Cloud climate (#25641) * enable climate away_mode and home.refresh * Add Party eco modes --- .../components/homematicip_cloud/climate.py | 17 ++++++++++++++--- .../components/homematicip_cloud/hap.py | 11 ++++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 53e7403ce56..794a8b44cbc 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -5,16 +5,19 @@ from typing import Awaitable from homematicip.aio.device import AsyncHeatingThermostat, AsyncHeatingThermostatCompact from homematicip.aio.group import AsyncHeatingGroup from homematicip.aio.home import AsyncHome +from homematicip.base.enums import AbsenceType +from homematicip.functionalHomes import IndoorClimateHome from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_HEAT, + PRESET_AWAY, PRESET_BOOST, PRESET_ECO, + PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - PRESET_NONE, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS @@ -116,9 +119,17 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): if self._device.boostMode: return PRESET_BOOST if self._device.controlMode == HMIP_ECO_CM: - return PRESET_ECO + absence_type = self._home.get_functionalHome(IndoorClimateHome).absenceType + if absence_type == AbsenceType.VACATION: + return PRESET_AWAY + if absence_type in [ + AbsenceType.PERIOD, + AbsenceType.PERMANENT, + AbsenceType.PARTY, + ]: + return PRESET_ECO - return None + return PRESET_NONE @property def preset_modes(self): diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 7418aa94d89..23973efb07b 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -110,10 +110,13 @@ class HomematicipHAP: Triggered when the HMIP HOME_CHANGED event has fired. There are several occasions for this event to happen. - We are only interested to check whether the access point + 1. We are interested to check whether the access point is still connected. If not, device state changes cannot be forwarded to hass. So if access point is disconnected all devices are set to unavailable. + 2. We need to update home including devices and groups after a reconnect. + 3. We need to update home without devices and groups in all other cases. + """ if not self.home.connected: _LOGGER.error("HMIP access point has lost connection with the cloud") @@ -127,6 +130,12 @@ class HomematicipHAP: job = self.hass.async_create_task(self.get_state()) job.add_done_callback(self.get_state_finished) + self._accesspoint_connected = True + else: + # Update home with the given json from arg[0], + # without devices and groups. + + self.home.update_home_only(args[0]) async def get_state(self): """Update HMIP state and tell Home Assistant.""" From f8bd9dbe3e2619fd889f5d6c9e85c5b959c51125 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 4 Aug 2019 14:32:35 +0200 Subject: [PATCH 035/273] Azure ci templates (#25688) * Use templates for Azure-CI steps * Migrate ci * fix endpoint --- azure-pipelines-ci.yml | 85 ++++++++----------------- azure-pipelines-wheels.yml | 127 +++++++++++++------------------------ 2 files changed, 70 insertions(+), 142 deletions(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 1e3d914aaf0..5297fd80231 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -18,9 +18,12 @@ resources: image: homeassistant/ci-azure:3.6 - container: 37 image: homeassistant/ci-azure:3.7 + repositories: + - repository: azure + type: github + name: 'home-assistant/ci-azure' + endpoint: 'home-assistant' variables: - - name: ArtifactFeed - value: '2df3ae11-3bf6-49bc-a809-ba0d340d6a6d' - name: PythonMain value: '36' - group: codecov @@ -95,39 +98,19 @@ stages: python.container: '37' container: $[ variables['python.container'] ] steps: - - script: | - python --version > .cache - displayName: 'Set python $(python.container) for requirement cache' - - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 - displayName: 'Restore artifacts based on Requirements' - inputs: + - template: templates/azp-step-cache.yaml@azure + parameters: keyfile: 'requirements_test_all.txt, .cache, homeassistant/package_constraints.txt' - targetfolder: './venv' - vstsFeed: '$(ArtifactFeed)' - - script: | - set -e - python -m venv venv + build: | + set -e + python -m venv venv - . venv/bin/activate - pip install -U pip setuptools pytest-azurepipelines -c homeassistant/package_constraints.txt - pip install -r requirements_test_all.txt -c homeassistant/package_constraints.txt - # This is a TEMP. Eventually we should make sure our 4 dependencies drop typing. - # Find offending deps with `pipdeptree -r -p typing` - pip uninstall -y typing - displayName: 'Create Virtual Environment & Install Requirements' - condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) - # Explicit Cache Save (instead of using RestoreAndSaveCache) - # Dont wait with cache save for all the other task in this job to complete (±30 minutes), other parallel jobs might utilize this - - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 - displayName: 'Save artifacts based on Requirements' - inputs: - keyfile: 'requirements_test_all.txt, .cache, homeassistant/package_constraints.txt' - targetfolder: './venv' - vstsFeed: '$(ArtifactFeed)' - - script: | - . venv/bin/activate - pip install -e . - displayName: 'Install Home Assistant for python $(python.container)' + . venv/bin/activate + pip install -U pip setuptools pytest-azurepipelines -c homeassistant/package_constraints.txt + pip install -r requirements_test_all.txt -c homeassistant/package_constraints.txt + # This is a TEMP. Eventually we should make sure our 4 dependencies drop typing. + # Find offending deps with `pipdeptree -r -p typing` + pip uninstall -y typing - script: | . venv/bin/activate pytest --timeout=9 --durations=10 --junitxml=test-results.xml -qq -o console_output_style=count -p no:sugar tests @@ -162,35 +145,17 @@ stages: vmImage: 'ubuntu-latest' container: $[ variables['PythonMain'] ] steps: - - script: | - python --version > .cache - displayName: 'Set python $(PythonMain) for requirement cache' - - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 - displayName: 'Restore artifacts based on Requirements' - inputs: + - template: templates/azp-step-cache.yaml@azure + parameters: keyfile: 'requirements_all.txt, requirements_test.txt, .cache, homeassistant/package_constraints.txt' - targetfolder: './venv' - vstsFeed: '$(ArtifactFeed)' - - script: | - set -e - python -m venv venv + build: | + set -e + python -m venv venv - . venv/bin/activate - pip install -U pip setuptools - pip install -r requirements_all.txt -c homeassistant/package_constraints.txt - pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - displayName: 'Create Virtual Environment & Install Requirements' - condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) - - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 - displayName: 'Save artifacts based on Requirements' - inputs: - keyfile: 'requirements_all.txt, requirements_test.txt, .cache, homeassistant/package_constraints.txt' - targetfolder: './venv' - vstsFeed: '$(ArtifactFeed)' - - script: | - . venv/bin/activate - pip install -e . - displayName: 'Install Home Assistant for python $(PythonMain)' + . venv/bin/activate + pip install -U pip setuptools + pip install -r requirements_all.txt -c homeassistant/package_constraints.txt + pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - script: | . venv/bin/activate pylint homeassistant diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 89e45fc31da..7e1c9991d57 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -12,88 +12,51 @@ pr: none variables: - name: versionWheels value: '1.0-3.7-alpine3.10' - - group: wheels - +resources: + repositories: + - repository: azure + type: github + name: 'home-assistant/ci-azure' + endpoint: 'home-assistant' jobs: +- template: templates/azp-job-wheels.yaml@azure + parameters: + builderVersion: '$(versionWheels)' + builderApk: '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' + wheelsRequirement: 'requirements_wheels.txt' + wheelsRequirementDiff: 'requirements_diff.txt' + preBuild: + - script: | + cp requirements_all.txt requirements_wheels.txt + if [[ "$(Build.Reason)" =~ (Schedule|Manual) ]]; then + touch requirements_diff.txt + else + curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/home-assistant/master/requirements_all.txt + fi -- job: 'Wheels' - timeoutInMinutes: 360 - pool: - vmImage: 'ubuntu-latest' - strategy: - maxParallel: 5 - 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.Reason)" =~ (Schedule|Manual) ]]; then - touch requirements_diff.txt - else - curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/home-assistant/master/requirements_all.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' + 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' From 5a90b49e2788f5c5f81a29a13d33fff7645f7f42 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 4 Aug 2019 16:12:16 +0200 Subject: [PATCH 036/273] In some circumstances device.last_seen can be None (#25690) --- homeassistant/components/unifi/device_tracker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 9c645a072a5..8ab5140dc48 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -296,7 +296,9 @@ class UniFiDeviceTracker(ScannerEntity): ) if ( - dt_util.utcnow() - dt_util.utc_from_timestamp(float(self.device.last_seen)) + self.device.last_seen + and dt_util.utcnow() + - dt_util.utc_from_timestamp(float(self.device.last_seen)) ) < detection_time: return True return False From 576291779e221767a252298878ed6b0907fe1d71 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 4 Aug 2019 16:57:36 +0200 Subject: [PATCH 037/273] UniFi - reverse connectivity logic (#25691) * Make connectivity control in line with other implementations --- homeassistant/components/unifi/switch.py | 12 ++++++------ tests/components/unifi/test_switch.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 236ca02ab2d..2b7965d1095 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -266,8 +266,8 @@ class UniFiBlockClientSwitch(UniFiClient, SwitchDevice): @property def is_on(self): - """Return true if client is blocked.""" - return self.client.blocked + """Return true if client is allowed to connect.""" + return not self.client.blocked @property def available(self): @@ -275,9 +275,9 @@ class UniFiBlockClientSwitch(UniFiClient, SwitchDevice): return self.controller.available async def async_turn_on(self, **kwargs): - """Block client.""" - await self.controller.api.clients.async_block(self.client.mac) + """Turn on connectivity for client.""" + await self.controller.api.clients.async_unblock(self.client.mac) async def async_turn_off(self, **kwargs): - """Unblock client.""" - await self.controller.api.clients.async_unblock(self.client.mac) + """Turn off connectivity for client.""" + await self.controller.api.clients.async_block(self.client.mac) diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 915646b9856..f84efa5dada 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -342,11 +342,11 @@ async def test_switches(hass, mock_controller): blocked = hass.states.get("switch.block_client_1") assert blocked is not None - assert blocked.state == "on" + assert blocked.state == "off" unblocked = hass.states.get("switch.block_client_2") assert unblocked is not None - assert unblocked.state == "off" + assert unblocked.state == "on" async def test_new_client_discovered(hass, mock_controller): From 739e8ffee8457ed55ea3c244bc6f829b2d488265 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 4 Aug 2019 17:04:29 +0200 Subject: [PATCH 038/273] Add schedules to yml --- azure-pipelines-wheels.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 7e1c9991d57..d467cf970b8 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -9,6 +9,13 @@ trigger: include: - requirements_all.txt pr: none +schedules: +- cron: "0 */8 * * *" + displayName: daily builds + branches: + include: + - dev + always: true variables: - name: versionWheels value: '1.0-3.7-alpine3.10' From 49a5dda7a8728712212726b9beafc899f1dbe992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 4 Aug 2019 18:05:43 +0300 Subject: [PATCH 039/273] Upgrade pydocstyle to 4.0.0, do not run in tox (#25667) * Upgrade pydocstyle to 4.0.0 and flake8-docstrings to 1.3.1 http://www.pydocstyle.org/en/4.0.0/release_notes.html#july-6th-2019 * Address pydocstyle D413's * tox: do not run pydocstyle Does not seem to add any value over flake8-docstrings (and would have needed a D202 exclusion). --- homeassistant/components/filter/sensor.py | 66 +++++----- .../components/islamic_prayer_times/sensor.py | 1 + homeassistant/components/lw12wifi/light.py | 5 +- .../components/wunderground/sensor.py | 121 ++++++++---------- homeassistant/util/ruamel_yaml.py | 1 + homeassistant/util/yaml/loader.py | 1 + requirements_test.txt | 4 +- requirements_test_all.txt | 4 +- tox.ini | 1 - 9 files changed, 100 insertions(+), 104 deletions(-) diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index b1ce967d6cd..ccb0a5b12f5 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -6,6 +6,7 @@ from numbers import Number from functools import partial from copy import copy from datetime import timedelta +from typing import Optional import voluptuous as vol @@ -333,17 +334,21 @@ class FilterState: class Filter: - """Filter skeleton. + """Filter skeleton.""" - Args: - window_size (int): size of the sliding window that holds previous - values - precision (int): round filtered value to precision value - entity (string): used for debugging only - """ + def __init__( + self, + name, + window_size: int = 1, + precision: Optional[int] = None, + entity: Optional[str] = None, + ): + """Initialize common attributes. - def __init__(self, name, window_size=1, precision=None, entity=None): - """Initialize common attributes.""" + :param window_size: size of the sliding window that holds previous values + :param precision: round filtered value to precision value + :param entity: used for debugging only + """ if isinstance(window_size, int): self.states = deque(maxlen=window_size) self.window_unit = WINDOW_SIZE_UNIT_NUMBER_EVENTS @@ -394,14 +399,19 @@ class RangeFilter(Filter): Determines if new state is in the range of upper_bound and lower_bound. If not inside, lower or upper bound is returned instead. - - Args: - upper_bound (float): band upper bound - lower_bound (float): band lower bound """ - def __init__(self, entity, lower_bound=None, upper_bound=None): - """Initialize Filter.""" + def __init__( + self, + entity, + lower_bound: Optional[float] = None, + upper_bound: Optional[float] = None, + ): + """Initialize Filter. + + :param upper_bound: band upper bound + :param lower_bound: band lower bound + """ super().__init__(FILTER_NAME_RANGE, entity=entity) self._lower_bound = lower_bound self._upper_bound = upper_bound @@ -441,13 +451,13 @@ class OutlierFilter(Filter): """BASIC outlier filter. Determines if new state is in a band around the median. - - Args: - radius (float): band radius """ - def __init__(self, window_size, precision, entity, radius): - """Initialize Filter.""" + def __init__(self, window_size, precision, entity, radius: float): + """Initialize Filter. + + :param radius: band radius + """ super().__init__(FILTER_NAME_OUTLIER, window_size, precision, entity) self._radius = radius self._stats_internal = Counter() @@ -475,13 +485,9 @@ class OutlierFilter(Filter): @FILTERS.register(FILTER_NAME_LOWPASS) class LowPassFilter(Filter): - """BASIC Low Pass Filter. + """BASIC Low Pass Filter.""" - Args: - time_constant (int): time constant. - """ - - def __init__(self, window_size, precision, entity, time_constant): + def __init__(self, window_size, precision, entity, time_constant: int): """Initialize Filter.""" super().__init__(FILTER_NAME_LOWPASS, window_size, precision, entity) self._time_constant = time_constant @@ -505,15 +511,15 @@ class TimeSMAFilter(Filter): """Simple Moving Average (SMA) Filter. The window_size is determined by time, and SMA is time weighted. - - Args: - type (enum): type of algorithm used to connect discrete values """ def __init__( self, window_size, precision, entity, type ): # pylint: disable=redefined-builtin - """Initialize Filter.""" + """Initialize Filter. + + :param type: type of algorithm used to connect discrete values + """ super().__init__(FILTER_NAME_TIME_SMA, window_size, precision, entity) self._time_window = window_size self.last_leak = None diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 44a27963971..88cbd2cb431 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -89,6 +89,7 @@ async def schedule_future_update(hass, sensors, midnight_time, prayer_times_data calculated midnight = 1:35AM (after traditional midnight) update time: 1:36AM. + """ _LOGGER.debug("Scheduling next update for Islamic prayer times") diff --git a/homeassistant/components/lw12wifi/light.py b/homeassistant/components/lw12wifi/light.py index d4b7a59e79c..3b9ccae1681 100644 --- a/homeassistant/components/lw12wifi/light.py +++ b/homeassistant/components/lw12wifi/light.py @@ -55,9 +55,8 @@ class LW12WiFi(Light): def __init__(self, name, lw12_light): """Initialise LW-12 WiFi LED Controller. - Args: - name: Friendly name for this platform to use. - lw12_light: Instance of the LW12 controller. + :param name: Friendly name for this platform to use. + :param lw12_light: Instance of the LW12 controller. """ self._light = lw12_light self._name = name diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py index 03a84ca4599..21f87d9ce0b 100644 --- a/homeassistant/components/wunderground/sensor.py +++ b/homeassistant/components/wunderground/sensor.py @@ -3,6 +3,7 @@ import asyncio from datetime import timedelta import logging import re +from typing import Any, Callable, Optional, Union import aiohttp import async_timeout @@ -56,29 +57,25 @@ class WUSensorConfig: def __init__( self, - friendly_name, - feature, - value, - unit_of_measurement=None, + friendly_name: Union[str, Callable], + feature: str, + value: Callable[["WUndergroundData"], Any], + unit_of_measurement: Optional[str] = None, entity_picture=None, - icon="mdi:gauge", + icon: str = "mdi:gauge", device_state_attributes=None, device_class=None, ): """Constructor. - Args: - friendly_name (string|func): Friendly name - feature (string): WU feature. See: + :param friendly_name: Friendly name + :param feature: WU feature. See: https://www.wunderground.com/weather/api/d/docs?d=data/index - value (function(WUndergroundData)): callback that - extracts desired value from WUndergroundData object - unit_of_measurement (string): unit of measurement - entity_picture (string): value or callback returning - URL of entity picture - icon (string): icon name or URL - device_state_attributes (dict): dictionary of attributes, - or callable that returns it + :param value: callback that extracts desired value from WUndergroundData object + :param unit_of_measurement: unit of measurement + :param entity_picture: value or callback returning URL of entity picture + :param icon: icon name or URL + :param device_state_attributes: dictionary of attributes, or callable that returns it """ self.friendly_name = friendly_name self.unit_of_measurement = unit_of_measurement @@ -95,21 +92,18 @@ class WUCurrentConditionsSensorConfig(WUSensorConfig): def __init__( self, - friendly_name, - field, - icon="mdi:gauge", - unit_of_measurement=None, + friendly_name: Union[str, Callable], + field: str, + icon: Optional[str] = "mdi:gauge", + unit_of_measurement: Optional[str] = None, device_class=None, ): """Constructor. - Args: - friendly_name (string|func): Friendly name of sensor - field (string): Field name in the "current_observation" - dictionary. - icon (string): icon name or URL, if None sensor - will use current weather symbol - unit_of_measurement (string): unit of measurement + :param friendly_name: Friendly name of sensor + :field: Field name in the "current_observation" dictionary. + :icon: icon name or URL, if None sensor will use current weather symbol + :unit_of_measurement: unit of measurement """ super().__init__( friendly_name, @@ -130,13 +124,14 @@ class WUCurrentConditionsSensorConfig(WUSensorConfig): class WUDailyTextForecastSensorConfig(WUSensorConfig): """Helper for defining sensor configurations for daily text forecasts.""" - def __init__(self, period, field, unit_of_measurement=None): + def __init__( + self, period: int, field: str, unit_of_measurement: Optional[str] = None + ): """Constructor. - Args: - period (int): forecast period number - field (string): field name to use as value - unit_of_measurement(string): unit of measurement + :param period: forecast period number + :param field: field name to use as value + :param unit_of_measurement: unit of measurement """ super().__init__( friendly_name=lambda wu: wu.data["forecast"]["txt_forecast"]["forecastday"][ @@ -161,24 +156,22 @@ class WUDailySimpleForecastSensorConfig(WUSensorConfig): def __init__( self, - friendly_name, - period, - field, - wu_unit=None, - ha_unit=None, + friendly_name: str, + period: int, + field: str, + wu_unit: Optional[str] = None, + ha_unit: Optional[str] = None, icon=None, device_class=None, ): """Constructor. - Args: - period (int): forecast period number - field (string): field name to use as value - wu_unit (string): "fahrenheit", "celsius", "degrees" etc. - see the example json at: - https://www.wunderground.com/weather/api/d/docs?d=data/forecast&MR=1 - ha_unit (string): corresponding unit in home assistant - title (string): friendly_name of the sensor + :param friendly_name: friendly_name of the sensor + :param period: forecast period number + :param field: field name to use as value + :param wu_unit: "fahrenheit", "celsius", "degrees" etc. see the example json at: + https://www.wunderground.com/weather/api/d/docs?d=data/forecast&MR=1 + :param ha_unit: corresponding unit in home assistant """ super().__init__( friendly_name=friendly_name, @@ -213,12 +206,11 @@ class WUDailySimpleForecastSensorConfig(WUSensorConfig): class WUHourlyForecastSensorConfig(WUSensorConfig): """Helper for defining sensor configurations for hourly text forecasts.""" - def __init__(self, period, field): + def __init__(self, period: int, field: int): """Constructor. - Args: - period (int): forecast period number - field (int): field name to use as value + :param period: forecast period number + :param field: field name to use as value """ super().__init__( friendly_name=lambda wu: "{} {}".format( @@ -274,24 +266,22 @@ class WUAlmanacSensorConfig(WUSensorConfig): def __init__( self, - friendly_name, - field, - value_type, - wu_unit, - unit_of_measurement, - icon, + friendly_name: Union[str, Callable], + field: str, + value_type: str, + wu_unit: str, + unit_of_measurement: str, + icon: str, device_class=None, ): """Constructor. - Args: - friendly_name (string|func): Friendly name - field (string): value name returned in 'almanac' dict - as returned by the WU API - value_type (string): "record" or "normal" - wu_unit (string): unit name in WU API - icon (string): icon name or URL - unit_of_measurement (string): unit of measurement + :param friendly_name: Friendly name + :param field: value name returned in 'almanac' dict as returned by the WU API + :param value_type: "record" or "normal" + :param wu_unit: unit name in WU API + :param unit_of_measurement: unit of measurement + :param icon: icon name or URL """ super().__init__( friendly_name=friendly_name, @@ -306,11 +296,10 @@ class WUAlmanacSensorConfig(WUSensorConfig): class WUAlertsSensorConfig(WUSensorConfig): """Helper for defining field configuration for alerts.""" - def __init__(self, friendly_name): + def __init__(self, friendly_name: Union[str, Callable]): """Constructor. - Args: - friendly_name (string|func): Friendly name + :param friendly_name: Friendly name """ super().__init__( friendly_name=friendly_name, diff --git a/homeassistant/util/ruamel_yaml.py b/homeassistant/util/ruamel_yaml.py index 931ad7adb91..70f447a3b0d 100644 --- a/homeassistant/util/ruamel_yaml.py +++ b/homeassistant/util/ruamel_yaml.py @@ -40,6 +40,7 @@ def _include_yaml( Example: device_tracker: !include device_tracker.yaml + """ if constructor.name is None: raise HomeAssistantError( diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index f5dfdb933f2..3cbf0481673 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -113,6 +113,7 @@ def _include_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE: Example: device_tracker: !include device_tracker.yaml + """ fname = os.path.join(os.path.dirname(loader.name), node.value) return _add_reference(load_yaml(fname), loader, node) diff --git a/requirements_test.txt b/requirements_test.txt index 7589c9ec27d..bcbc53f2649 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,11 +7,11 @@ asynctest==0.13.0 black==19.3b0 codecov==2.0.15 coveralls==1.2.0 -flake8-docstrings==1.3.0 +flake8-docstrings==1.3.1 flake8==3.7.8 mock-open==1.3.1 mypy==0.720 -pydocstyle==3.0.0 +pydocstyle==4.0.0 pylint==2.3.1 pytest-aiohttp==0.3.0 pytest-cov==2.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6549e9964ca..e92cf8a24bd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -8,11 +8,11 @@ asynctest==0.13.0 black==19.3b0 codecov==2.0.15 coveralls==1.2.0 -flake8-docstrings==1.3.0 +flake8-docstrings==1.3.1 flake8==3.7.8 mock-open==1.3.1 mypy==0.720 -pydocstyle==3.0.0 +pydocstyle==4.0.0 pylint==2.3.1 pytest-aiohttp==0.3.0 pytest-cov==2.7.1 diff --git a/tox.ini b/tox.ini index dd7206f403a..2d4cf7c54ba 100644 --- a/tox.ini +++ b/tox.ini @@ -35,7 +35,6 @@ commands = python -m script.gen_requirements_all validate python -m script.hassfest validate flake8 {posargs: homeassistant tests script} - pydocstyle {posargs:homeassistant tests} [testenv:typing] whitelist_externals=/bin/bash From 0d95ad38571bad468f7e03ee80b8a9eaf33138bf Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 4 Aug 2019 19:17:30 +0200 Subject: [PATCH 040/273] Plugwise haanna 0.10.1 (#25693) * Update module dependency (py3.6 fix) * Update module dependency (py3.6 fix) - update requirements --- homeassistant/components/plugwise/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 3a5f4ae0be4..c399232f315 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/components/plugwise", "dependencies": [], "codeowners": ["@laetificat","@CoMPaTech"], - "requirements": ["haanna==0.10.0"] + "requirements": ["haanna==0.10.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 28e702ee31d..a37437b6330 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -587,7 +587,7 @@ ha-ffmpeg==2.0 ha-philipsjs==0.0.8 # homeassistant.components.plugwise -haanna==0.10.0 +haanna==0.10.1 # homeassistant.components.habitica habitipy==0.2.0 From b0c79c271d7246acb5844a8e85e241d3d6535b68 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 4 Aug 2019 23:06:36 +0100 Subject: [PATCH 041/273] Bump geniushub client, handle dead devices, handle raise_for_status (#25687) * Initial commit * tweak error logging * bump client * correct regression * small coding tweak * debug logging to one entry * refactor for self.data['attr'] * bump client * small tidy-up --- .../components/geniushub/__init__.py | 53 ++++++++----------- .../components/geniushub/binary_sensor.py | 15 +++--- homeassistant/components/geniushub/climate.py | 24 ++++----- .../components/geniushub/manifest.json | 2 +- homeassistant/components/geniushub/sensor.py | 18 +++---- .../components/geniushub/water_heater.py | 15 ++---- requirements_all.txt | 2 +- 7 files changed, 50 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 3bbec4258bc..e4b723d595b 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import aiohttp import voluptuous as vol from geniushubclient import GeniusHubClient @@ -41,32 +42,23 @@ async def async_setup(hass, hass_config): args = (kwargs.pop(CONF_TOKEN),) hass.data[DOMAIN] = {} - data = hass.data[DOMAIN]["data"] = GeniusData(hass, args, kwargs) + broker = GeniusBroker(hass, args, kwargs) + try: - await data._client.hub.update() # pylint: disable=protected-access - except AssertionError: # assert response.status == HTTP_OK - _LOGGER.warning("Setup failed, check your configuration.", exc_info=True) + await broker._client.hub.update() # pylint: disable=protected-access + except aiohttp.ClientResponseError as err: + _LOGGER.error("Setup failed, check your configuration, %s", err) return False + broker.make_debug_log_entries() - _LOGGER.debug( - # noqa; pylint: disable=protected-access - "zones_raw = %s", - data._client.hub._zones_raw, - ) - _LOGGER.debug( - # noqa; pylint: disable=protected-access - "devices_raw = %s", - data._client.hub._devices_raw, - ) - - async_track_time_interval(hass, data.async_update, SCAN_INTERVAL) + async_track_time_interval(hass, broker.async_update, SCAN_INTERVAL) for platform in ["climate", "water_heater"]: hass.async_create_task( async_load_platform(hass, platform, DOMAIN, {}, hass_config) ) - if data._client.api_version == 3: # pylint: disable=protected-access + if broker._client.api_version == 3: # pylint: disable=protected-access for platform in ["sensor", "binary_sensor"]: hass.async_create_task( async_load_platform(hass, platform, DOMAIN, {}, hass_config) @@ -75,7 +67,7 @@ async def async_setup(hass, hass_config): return True -class GeniusData: +class GeniusBroker: """Container for geniushub client and data.""" def __init__(self, hass, args, kwargs): @@ -89,19 +81,18 @@ class GeniusData: """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) + except aiohttp.ClientResponseError as err: + _LOGGER.warning("Update failed, %s", err) return - - _LOGGER.debug( - # noqa; pylint: disable=protected-access - "zones_raw = %s", - self._client.hub._zones_raw, - ) - _LOGGER.debug( - # noqa; pylint: disable=protected-access - "devices_raw = %s", - self._client.hub._devices_raw, - ) + self.make_debug_log_entries() async_dispatcher_send(self._hass, DOMAIN) + + def make_debug_log_entries(self): + """Make any useful debug log entries.""" + # pylint: disable=protected-access + _LOGGER.debug( + "Raw JSON: \n\nhub._raw_zones = %s \n\nhub._raw_devices = %s", + self._client.hub._raw_zones, + self._client.hub._raw_devices, + ) diff --git a/homeassistant/components/geniushub/binary_sensor.py b/homeassistant/components/geniushub/binary_sensor.py index afdc0ef5f89..feb2e0da33e 100644 --- a/homeassistant/components/geniushub/binary_sensor.py +++ b/homeassistant/components/geniushub/binary_sensor.py @@ -1,6 +1,4 @@ """Support for Genius Hub binary_sensor devices.""" -import logging - from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -8,8 +6,6 @@ from homeassistant.util.dt import utc_from_timestamp from . import DOMAIN -_LOGGER = logging.getLogger(__name__) - GH_IS_SWITCH = ["Dual Channel Receiver", "Electric Switch", "Smart Plug"] @@ -17,9 +13,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up the Genius Hub sensor entities.""" client = hass.data[DOMAIN]["client"] - devices = [d for d in client.hub.device_objs if d.type is not None] switches = [ - GeniusBinarySensor(client, d) for d in devices if d.type[:21] in GH_IS_SWITCH + GeniusBinarySensor(client, d) + for d in client.hub.device_objs + if d.type[:21] in GH_IS_SWITCH ] async_add_entities(switches) @@ -59,16 +56,16 @@ class GeniusBinarySensor(BinarySensorDevice): @property def is_on(self): """Return the status of the sensor.""" - return self._device.state["outputOnOff"] + return self._device.data["state"]["outputOnOff"] @property def device_state_attributes(self): """Return the device state attributes.""" attrs = {} - attrs["assigned_zone"] = self._device.assignedZones[0]["name"] + attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] # noqa; pylint: disable=protected-access - last_comms = self._device._raw_json["childValues"]["lastComms"]["val"] + last_comms = self._device._raw_data["childValues"]["lastComms"]["val"] if last_comms != 0: attrs["last_comms"] = utc_from_timestamp(last_comms).isoformat() diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index ae1d714dd2b..cee737c09f8 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -1,5 +1,4 @@ """Support for Genius Hub climate devices.""" -import logging from typing import Any, Awaitable, Dict, Optional, List from homeassistant.components.climate import ClimateDevice @@ -17,8 +16,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DOMAIN -_LOGGER = logging.getLogger(__name__) - ATTR_DURATION = "duration" GH_ZONES = ["radiator"] @@ -40,13 +37,10 @@ async def async_setup_platform( """Set up the Genius Hub climate entities.""" client = hass.data[DOMAIN]["client"] - async_add_entities( - [ - GeniusClimateZone(client, z) - for z in client.hub.zone_objs - if z.type in GH_ZONES - ] - ) + entities = [ + GeniusClimateZone(client, z) for z in client.hub.zone_objs if z.type in GH_ZONES + ] + async_add_entities(entities) class GeniusClimateZone(ClimateDevice): @@ -78,7 +72,7 @@ class GeniusClimateZone(ClimateDevice): @property def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" - tmp = self._zone.__dict__.items() + tmp = self._zone.data.items() return {"status": {k: v for k, v in tmp if k in GH_STATE_ATTRS}} @property @@ -94,12 +88,12 @@ class GeniusClimateZone(ClimateDevice): @property def current_temperature(self) -> Optional[float]: """Return the current temperature.""" - return self._zone.temperature + return self._zone.data["temperature"] @property def target_temperature(self) -> Optional[float]: """Return the temperature we try to reach.""" - return self._zone.setpoint + return self._zone.data["setpoint"] @property def min_temp(self) -> float: @@ -124,7 +118,7 @@ class GeniusClimateZone(ClimateDevice): @property def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" - return GH_HVAC_TO_HA.get(self._zone.mode, HVAC_MODE_HEAT) + return GH_HVAC_TO_HA.get(self._zone.data["mode"], HVAC_MODE_HEAT) @property def hvac_modes(self) -> List[str]: @@ -134,7 +128,7 @@ class GeniusClimateZone(ClimateDevice): @property def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" - return GH_PRESET_TO_HA.get(self._zone.mode) + return GH_PRESET_TO_HA.get(self._zone.data["mode"]) @property def preset_modes(self) -> Optional[List[str]]: diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index 98145ea0944..0721c4ff389 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.5.4" + "geniushub-client==0.5.8" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index f87c957d3da..65bfcb7fe9b 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -1,6 +1,5 @@ """Support for Genius Hub sensor devices.""" from datetime import timedelta -import logging from homeassistant.const import DEVICE_CLASS_BATTERY from homeassistant.core import callback @@ -10,8 +9,6 @@ from homeassistant.util.dt import utc_from_timestamp, utcnow from . import DOMAIN -_LOGGER = logging.getLogger(__name__) - GH_HAS_BATTERY = ["Room Thermostat", "Genius Valve", "Room Sensor", "Radiator Valve"] GH_LEVEL_MAPPING = { @@ -26,17 +23,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= client = hass.data[DOMAIN]["client"] sensors = [ - GeniusDevice(client, d) + GeniusBattery(client, d) for d in client.hub.device_objs if d.type in GH_HAS_BATTERY ] - issues = [GeniusIssue(client, i) for i in list(GH_LEVEL_MAPPING)] async_add_entities(sensors + issues, update_before_add=True) -class GeniusDevice(Entity): +class GeniusBattery(Entity): """Representation of a Genius Hub sensor.""" def __init__(self, client, device): @@ -63,7 +59,7 @@ class GeniusDevice(Entity): def icon(self): """Return the icon of the sensor.""" # noqa; pylint: disable=protected-access - values = self._device._raw_json["childValues"] + values = self._device._raw_data["childValues"] last_comms = utc_from_timestamp(values["lastComms"]["val"]) if "WakeUp_Interval" in values: @@ -74,7 +70,7 @@ class GeniusDevice(Entity): if last_comms < utcnow() - interval * 3: return "mdi:battery-unknown" - battery_level = self._device.state["batteryLevel"] + battery_level = self._device.data["state"]["batteryLevel"] if battery_level == 255: return "mdi:battery-unknown" if battery_level < 40: @@ -104,17 +100,17 @@ class GeniusDevice(Entity): @property def state(self): """Return the state of the sensor.""" - level = self._device.state.get("batteryLevel", 255) + level = self._device.data["state"].get("batteryLevel", 255) return level if level != 255 else 0 @property def device_state_attributes(self): """Return the device state attributes.""" attrs = {} - attrs["assigned_zone"] = self._device.assignedZones[0]["name"] + attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] # noqa; pylint: disable=protected-access - last_comms = self._device._raw_json["childValues"]["lastComms"]["val"] + last_comms = self._device._raw_data["childValues"]["lastComms"]["val"] attrs["last_comms"] = utc_from_timestamp(last_comms).isoformat() return {**attrs} diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index 9e27ec5f190..feb4235d4dd 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -1,6 +1,4 @@ """Support for Genius Hub water_heater devices.""" -import logging - from homeassistant.components.water_heater import ( WaterHeaterDevice, SUPPORT_TARGET_TEMPERATURE, @@ -15,8 +13,6 @@ from . import DOMAIN STATE_AUTO = "auto" STATE_MANUAL = "manual" -_LOGGER = logging.getLogger(__name__) - GH_HEATERS = ["hot water temperature"] GH_SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE @@ -82,7 +78,7 @@ class GeniusWaterHeater(WaterHeaterDevice): @property def device_state_attributes(self): """Return the device state attributes.""" - tmp = self._boiler.__dict__.items() + tmp = self._boiler.data.items() return {"status": {k: v for k, v in tmp if k in GH_STATE_ATTRS}} @property @@ -93,15 +89,12 @@ class GeniusWaterHeater(WaterHeaterDevice): @property def current_temperature(self): """Return the current temperature.""" - try: - return self._boiler.temperature - except AttributeError: - return None + return self._boiler.data.get("temperature") @property def target_temperature(self): """Return the temperature we try to reach.""" - return self._boiler.setpoint + return self._boiler.data["setpoint"] @property def min_temp(self): @@ -131,7 +124,7 @@ class GeniusWaterHeater(WaterHeaterDevice): @property def current_operation(self): """Return the current operation mode.""" - return GH_STATE_TO_HA[self._boiler.mode] + return GH_STATE_TO_HA[self._boiler.data["mode"]] async def async_set_operation_mode(self, operation_mode): """Set a new operation mode for this boiler.""" diff --git a/requirements_all.txt b/requirements_all.txt index a37437b6330..2a610756ca1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -517,7 +517,7 @@ gearbest_parser==1.0.7 geizhals==0.0.9 # homeassistant.components.geniushub -geniushub-client==0.5.4 +geniushub-client==0.5.8 # homeassistant.components.geo_json_events # homeassistant.components.nsw_rural_fire_service_feed From 3839eb019773fb723ad867a9992dc3cb431ae417 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 4 Aug 2019 16:11:28 -0600 Subject: [PATCH 042/273] Fix issue with incorrect Notion bridge IDs (#25683) * Fix issue with incorrect Notion bridge IDs * Less aggressive * Member comments --- homeassistant/components/notion/__init__.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index d2c45330bdb..62deb4999d9 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -235,7 +235,7 @@ class NotionEntity(Entity): @property def device_info(self): """Return device registry information for this entity.""" - bridge = self._notion.bridges[self._bridge_id] + bridge = self._notion.bridges.get(self._bridge_id, {}) sensor = self._notion.sensors[self._sensor_id] return { @@ -244,7 +244,7 @@ class NotionEntity(Entity): "model": sensor["hardware_revision"], "name": sensor["name"], "sw_version": sensor["firmware_version"], - "via_device": (DOMAIN, bridge["hardware_id"]), + "via_device": (DOMAIN, bridge.get("hardware_id")), } @property @@ -271,7 +271,14 @@ class NotionEntity(Entity): Sensors can move to other bridges based on signal strength, etc. """ sensor = self._notion.sensors[self._sensor_id] - if self._bridge_id == sensor["bridge"]["id"]: + + # If the sensor's bridge ID is the same as what we had before or if it points + # to a bridge that doesn't exist (which can happen due to a Notion API bug), + # return immediately: + if ( + self._bridge_id == sensor["bridge"]["id"] + or sensor["bridge"]["id"] not in self._notion.bridges + ): return self._bridge_id = sensor["bridge"]["id"] From 03aec33f9e70d6a516f144506831531753df8be6 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 5 Aug 2019 00:13:27 +0200 Subject: [PATCH 043/273] Fix roku lxml requirement (#25696) --- homeassistant/components/roku/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 6bdc1f6bf3d..477bcb105f7 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -3,7 +3,7 @@ "name": "Roku", "documentation": "https://www.home-assistant.io/components/roku", "requirements": [ - "roku==3.0.0" + "roku==3.1" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 2a610756ca1..850c98c71b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1644,7 +1644,7 @@ rjpl==0.3.5 rocketchat-API==0.6.1 # homeassistant.components.roku -roku==3.0.0 +roku==3.1 # homeassistant.components.roomba roombapy==1.3.1 From f7cfe908f7b4168cf10f89e2432ecc29c8d23989 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 4 Aug 2019 18:20:03 -0400 Subject: [PATCH 044/273] Refactor ZHA Zigbee Cluster report configuration (#25589) * Move ZCL report configs to ZHA core channels. * Refactor ZCL report configuratopm and cluster binding. * Tests for ZHA channel configuration. * Update tests. * Remove INPUT_BIND_ONLY_CLUSTER ZHA core registry. We always need bind a cluster, but not always need to configure attribute reporting. No reporting is done on ZCL "client" clusters. * Lint * Black --- .../components/zha/core/channels/__init__.py | 98 +++++++++---- .../components/zha/core/channels/closures.py | 3 +- .../components/zha/core/channels/general.py | 16 ++- .../zha/core/channels/homeautomation.py | 7 +- .../components/zha/core/channels/hvac.py | 3 +- .../components/zha/core/channels/lighting.py | 6 + .../components/zha/core/channels/security.py | 6 +- homeassistant/components/zha/core/gateway.py | 10 +- homeassistant/components/zha/core/helpers.py | 109 +------------- .../components/zha/core/registries.py | 45 ------ tests/components/zha/common.py | 5 +- tests/components/zha/test_channels.py | 136 ++++++++++++++++++ 12 files changed, 245 insertions(+), 199 deletions(-) create mode 100644 tests/components/zha/test_channels.py diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 3a754fce2ec..21bdf8a5575 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -18,17 +18,12 @@ from ..const import ( CHANNEL_ATTRIBUTE, CHANNEL_EVENT_RELAY, CHANNEL_ZDO, - REPORT_CONFIG_DEFAULT, + REPORT_CONFIG_MAX_INT, + REPORT_CONFIG_MIN_INT, + REPORT_CONFIG_RPT_CHANGE, SIGNAL_ATTR_UPDATED, ) -from ..helpers import ( - LogMixin, - bind_cluster, - configure_reporting, - construct_unique_id, - get_attr_id_by_name, - safe_read, -) +from ..helpers import LogMixin, construct_unique_id, get_attr_id_by_name, safe_read from ..registries import CLUSTER_REPORT_CONFIGS _LOGGER = logging.getLogger(__name__) @@ -84,6 +79,7 @@ class ZigbeeChannel(LogMixin): """Base channel for a Zigbee cluster.""" CHANNEL_NAME = None + REPORT_CONFIG = () def __init__(self, cluster, device): """Initialize ZigbeeChannel.""" @@ -95,7 +91,7 @@ class ZigbeeChannel(LogMixin): self._zha_device = device self._unique_id = construct_unique_id(cluster) self._report_config = CLUSTER_REPORT_CONFIGS.get( - self._cluster.cluster_id, [{"attr": 0, "config": REPORT_CONFIG_DEFAULT}] + self._cluster.cluster_id, self.REPORT_CONFIG ) self._status = ChannelStatus.CREATED self._cluster.add_listener(self) @@ -134,29 +130,75 @@ class ZigbeeChannel(LogMixin): """Set the reporting configuration.""" self._report_config = report_config + async def bind(self): + """Bind a zigbee cluster. + + This also swallows DeliveryError exceptions that are thrown when + devices are unreachable. + """ + from zigpy.exceptions import DeliveryError + + try: + res = await self.cluster.bind() + self.debug("bound '%s' cluster: %s", self.cluster.ep_attribute, res[0]) + except (DeliveryError, Timeout) as ex: + self.debug( + "Failed to bind '%s' cluster: %s", self.cluster.ep_attribute, str(ex) + ) + + async def configure_reporting( + self, + attr, + report_config=( + REPORT_CONFIG_MIN_INT, + REPORT_CONFIG_MAX_INT, + REPORT_CONFIG_RPT_CHANGE, + ), + ): + """Configure attribute reporting for a cluster. + + This also swallows DeliveryError exceptions that are thrown when + devices are unreachable. + """ + from zigpy.exceptions import DeliveryError + + attr_name = self.cluster.attributes.get(attr, [attr])[0] + + kwargs = {} + if self.cluster.cluster_id >= 0xFC00 and self.device.manufacturer_code: + kwargs["manufacturer"] = self.device.manufacturer_code + + min_report_int, max_report_int, reportable_change = report_config + try: + res = await self.cluster.configure_reporting( + attr, min_report_int, max_report_int, reportable_change, **kwargs + ) + self.debug( + "reporting '%s' attr on '%s' cluster: %d/%d/%d: Result: '%s'", + attr_name, + self.cluster.ep_attribute, + min_report_int, + max_report_int, + reportable_change, + res, + ) + except (DeliveryError, Timeout) as ex: + self.debug( + "failed to set reporting for '%s' attr on '%s' cluster: %s", + attr_name, + self.cluster.ep_attribute, + str(ex), + ) + async def async_configure(self): """Set cluster binding and attribute reporting.""" - manufacturer = None - manufacturer_code = self._zha_device.manufacturer_code # Xiaomi devices don't need this and it disrupts pairing if self._zha_device.manufacturer != "LUMI": - if self.cluster.cluster_id >= 0xFC00 and manufacturer_code: - manufacturer = manufacturer_code - await bind_cluster(self._unique_id, self.cluster) - if not self.cluster.bind_only: + await self.bind() + if self.cluster.cluster_id not in self.cluster.endpoint.out_clusters: for report_config in self._report_config: - attr = report_config.get("attr") - min_report_interval, max_report_interval, change = report_config.get( - "config" - ) - await configure_reporting( - self._unique_id, - self.cluster, - attr, - min_report=min_report_interval, - max_report=max_report_interval, - reportable_change=change, - manufacturer=manufacturer, + await self.configure_reporting( + report_config["attr"], report_config["config"] ) await asyncio.sleep(uniform(0.1, 0.5)) diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index 23d174c08b1..87a331984a5 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -10,7 +10,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from . import ZigbeeChannel -from ..const import SIGNAL_ATTR_UPDATED +from ..const import REPORT_CONFIG_IMMEDIATE, SIGNAL_ATTR_UPDATED _LOGGER = logging.getLogger(__name__) @@ -19,6 +19,7 @@ class DoorLockChannel(ZigbeeChannel): """Door lock channel.""" _value_attribute = 0 + REPORT_CONFIG = ({"attr": "lock_state", "config": REPORT_CONFIG_IMMEDIATE},) async def async_update(self): """Retrieve latest state.""" diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 388524b62e4..bc33c2d34f5 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -11,7 +11,14 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later from . import ZigbeeChannel, parse_and_log_command -from ..const import SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL +from ..const import ( + SIGNAL_ATTR_UPDATED, + SIGNAL_MOVE_LEVEL, + SIGNAL_SET_LEVEL, + REPORT_CONFIG_ASAP, + REPORT_CONFIG_BATTERY_SAVE, + REPORT_CONFIG_IMMEDIATE, +) from ..helpers import get_attr_id_by_name _LOGGER = logging.getLogger(__name__) @@ -21,6 +28,7 @@ class OnOffChannel(ZigbeeChannel): """Channel for the OnOff Zigbee cluster.""" ON_OFF = 0 + REPORT_CONFIG = ({"attr": "on_off", "config": REPORT_CONFIG_IMMEDIATE},) def __init__(self, cluster, device): """Initialize OnOffChannel.""" @@ -93,6 +101,7 @@ class LevelControlChannel(ZigbeeChannel): """Channel for the LevelControl Zigbee cluster.""" CURRENT_LEVEL = 0 + REPORT_CONFIG = ({"attr": "current_level", "config": REPORT_CONFIG_ASAP},) @callback def cluster_command(self, tsn, command_id, args): @@ -173,6 +182,11 @@ class BasicChannel(ZigbeeChannel): class PowerConfigurationChannel(ZigbeeChannel): """Channel for the zigbee power configuration cluster.""" + REPORT_CONFIG = ( + {"attr": "battery_voltage", "config": REPORT_CONFIG_BATTERY_SAVE}, + {"attr": "battery_percentage_remaining", "config": REPORT_CONFIG_BATTERY_SAVE}, + ) + @callback def attribute_updated(self, attrid, value): """Handle attribute updates on this cluster.""" diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index 05b9b591cac..f0888d29682 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -9,7 +9,11 @@ import logging from homeassistant.helpers.dispatcher import async_dispatcher_send from . import AttributeListeningChannel -from ..const import CHANNEL_ELECTRICAL_MEASUREMENT, SIGNAL_ATTR_UPDATED +from ..const import ( + CHANNEL_ELECTRICAL_MEASUREMENT, + REPORT_CONFIG_DEFAULT, + SIGNAL_ATTR_UPDATED, +) _LOGGER = logging.getLogger(__name__) @@ -18,6 +22,7 @@ class ElectricalMeasurementChannel(AttributeListeningChannel): """Channel that polls active power level.""" CHANNEL_NAME = CHANNEL_ELECTRICAL_MEASUREMENT + REPORT_CONFIG = ({"attr": "active_power", "config": REPORT_CONFIG_DEFAULT},) async def async_update(self): """Retrieve latest state.""" diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 7e38af4a0d7..c9c809ce245 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -10,7 +10,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from . import ZigbeeChannel -from ..const import SIGNAL_ATTR_UPDATED +from ..const import REPORT_CONFIG_OP, SIGNAL_ATTR_UPDATED _LOGGER = logging.getLogger(__name__) @@ -19,6 +19,7 @@ class FanChannel(ZigbeeChannel): """Fan channel.""" _value_attribute = 0 + REPORT_CONFIG = ({"attr": "fan_mode", "config": REPORT_CONFIG_OP},) async def async_set_speed(self, value) -> None: """Set the speed of the fan.""" diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index 05d88d82288..11762c0fe00 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/zha/ import logging from . import ZigbeeChannel +from ..const import REPORT_CONFIG_DEFAULT _LOGGER = logging.getLogger(__name__) @@ -17,6 +18,11 @@ class ColorChannel(ZigbeeChannel): CAPABILITIES_COLOR_XY = 0x08 CAPABILITIES_COLOR_TEMP = 0x10 UNSUPPORTED_ATTRIBUTE = 0x86 + REPORT_CONFIG = ( + {"attr": "current_x", "config": REPORT_CONFIG_DEFAULT}, + {"attr": "current_y", "config": REPORT_CONFIG_DEFAULT}, + {"attr": "color_temperature", "config": REPORT_CONFIG_DEFAULT}, + ) def __init__(self, cluster, device): """Initialize ColorChannel.""" diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 20b5ce7ba8f..c7f2366d097 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -11,7 +11,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from . import ZigbeeChannel from ..const import SIGNAL_ATTR_UPDATED -from ..helpers import bind_cluster _LOGGER = logging.getLogger(__name__) @@ -39,13 +38,14 @@ class IASZoneChannel(ZigbeeChannel): """Configure IAS device.""" # Xiaomi devices don't need this and it disrupts pairing if self._zha_device.manufacturer == "LUMI": + self.debug("%s: finished IASZoneChannel configuration") return from zigpy.exceptions import DeliveryError self.debug("started IASZoneChannel configuration") - await bind_cluster(self.unique_id, self._cluster) - ieee = self._cluster.endpoint.device.application.ieee + await self.bind() + ieee = self.cluster.endpoint.device.application.ieee try: res = await self._cluster.write_attributes({"cie_addr": ieee}) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index bf8eadb8f53..5ac02293107 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -65,7 +65,7 @@ from .const import ( from .device import DeviceStatus, ZHADevice from .discovery import async_dispatch_discovery_info, async_process_endpoint from .patches import apply_application_controller_patch -from .registries import INPUT_BIND_ONLY_CLUSTERS, RADIO_TYPES +from .registries import RADIO_TYPES from .store import async_get_registry _LOGGER = logging.getLogger(__name__) @@ -342,14 +342,6 @@ class ZHAGateway: zha_device, is_new_join, ) - if endpoint_id != 0: - for cluster in endpoint.in_clusters.values(): - cluster.bind_only = ( - cluster.cluster_id in INPUT_BIND_ONLY_CLUSTERS - ) - for cluster in endpoint.out_clusters.values(): - # output clusters are always bind only - cluster.bind_only = True else: is_rejoin = is_new_join is True _LOGGER.debug( diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index fb35ad35b98..9e7b7efbfd9 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -6,20 +6,11 @@ https://home-assistant.io/components/zha/ """ import asyncio import collections -from concurrent.futures import TimeoutError as Timeout import logging from homeassistant.core import callback -from .const import ( - CLUSTER_TYPE_IN, - CLUSTER_TYPE_OUT, - DEFAULT_BAUDRATE, - REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_MIN_INT, - REPORT_CONFIG_RPT_CHANGE, - RadioType, -) +from .const import CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, DEFAULT_BAUDRATE, RadioType from .registries import BINDABLE_CLUSTERS _LOGGER = logging.getLogger(__name__) @@ -48,104 +39,6 @@ async def safe_read( return {} -async def bind_cluster(entity_id, cluster): - """Bind a zigbee cluster. - - This also swallows DeliveryError exceptions that are thrown when devices - are unreachable. - """ - from zigpy.exceptions import DeliveryError - - cluster_name = cluster.ep_attribute - try: - res = await cluster.bind() - _LOGGER.debug("%s: bound '%s' cluster: %s", entity_id, cluster_name, res[0]) - except (DeliveryError, Timeout) as ex: - _LOGGER.debug( - "%s: Failed to bind '%s' cluster: %s", entity_id, cluster_name, str(ex) - ) - - -async def configure_reporting( - entity_id, - cluster, - attr, - min_report=REPORT_CONFIG_MIN_INT, - max_report=REPORT_CONFIG_MAX_INT, - reportable_change=REPORT_CONFIG_RPT_CHANGE, - manufacturer=None, -): - """Configure attribute reporting for a cluster. - - This also swallows DeliveryError exceptions that are thrown when devices - are unreachable. - """ - from zigpy.exceptions import DeliveryError - - attr_name = cluster.attributes.get(attr, [attr])[0] - - if isinstance(attr, str): - attr_id = get_attr_id_by_name(cluster, attr_name) - else: - attr_id = attr - - cluster_name = cluster.ep_attribute - kwargs = {} - if manufacturer: - kwargs["manufacturer"] = manufacturer - try: - res = await cluster.configure_reporting( - attr_id, min_report, max_report, reportable_change, **kwargs - ) - _LOGGER.debug( - "%s: reporting '%s' attr on '%s' cluster: %d/%d/%d: Result: '%s'", - entity_id, - attr_name, - cluster_name, - min_report, - max_report, - reportable_change, - res, - ) - except (DeliveryError, Timeout) as ex: - _LOGGER.debug( - "%s: failed to set reporting for '%s' attr on '%s' cluster: %s", - entity_id, - attr_name, - cluster_name, - str(ex), - ) - - -async def bind_configure_reporting( - entity_id, - cluster, - attr, - skip_bind=False, - min_report=REPORT_CONFIG_MIN_INT, - max_report=REPORT_CONFIG_MAX_INT, - reportable_change=REPORT_CONFIG_RPT_CHANGE, - manufacturer=None, -): - """Bind and configure zigbee attribute reporting for a cluster. - - This also swallows DeliveryError exceptions that are thrown when devices - are unreachable. - """ - if not skip_bind: - await bind_cluster(entity_id, cluster) - - await configure_reporting( - entity_id, - cluster, - attr, - min_report=min_report, - max_report=max_report, - reportable_change=reportable_change, - manufacturer=manufacturer, - ) - - async def check_zigpy_connection(usb_path, radio_type, database_path): """Test zigpy radio connection.""" if radio_type == RadioType.ezsp.name: diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index d4b198e394e..ec05ec19551 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -20,7 +20,6 @@ from .const import ( REPORT_CONFIG_IMMEDIATE, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MIN_INT, - REPORT_CONFIG_OP, SENSOR_ACCELERATION, SENSOR_BATTERY, SENSOR_ELECTRICAL_MEASUREMENT, @@ -46,7 +45,6 @@ CUSTOM_CLUSTER_MAPPINGS = {} DEVICE_CLASS = {} DEVICE_TRACKER_CLUSTERS = set() EVENT_RELAY_CLUSTERS = [] -INPUT_BIND_ONLY_CLUSTERS = [] LIGHT_CLUSTERS = set() OUTPUT_CHANNEL_ONLY_CLUSTERS = [] RADIO_TYPES = {} @@ -145,31 +143,6 @@ def establish_device_mappings(): CLUSTER_REPORT_CONFIGS.update( { - zcl.clusters.general.Alarms.cluster_id: [], - zcl.clusters.general.Basic.cluster_id: [], - zcl.clusters.general.Commissioning.cluster_id: [], - zcl.clusters.general.Identify.cluster_id: [], - zcl.clusters.general.Groups.cluster_id: [], - zcl.clusters.general.Scenes.cluster_id: [], - zcl.clusters.general.Partition.cluster_id: [], - zcl.clusters.general.Ota.cluster_id: [], - zcl.clusters.general.PowerProfile.cluster_id: [], - zcl.clusters.general.ApplianceControl.cluster_id: [], - zcl.clusters.general.PollControl.cluster_id: [], - zcl.clusters.general.GreenPowerProxy.cluster_id: [], - zcl.clusters.general.OnOffConfiguration.cluster_id: [], - zcl.clusters.lightlink.LightLink.cluster_id: [], - zcl.clusters.general.OnOff.cluster_id: [ - {"attr": "on_off", "config": REPORT_CONFIG_IMMEDIATE} - ], - zcl.clusters.general.LevelControl.cluster_id: [ - {"attr": "current_level", "config": REPORT_CONFIG_ASAP} - ], - zcl.clusters.lighting.Color.cluster_id: [ - {"attr": "current_x", "config": REPORT_CONFIG_DEFAULT}, - {"attr": "current_y", "config": REPORT_CONFIG_DEFAULT}, - {"attr": "color_temperature", "config": REPORT_CONFIG_DEFAULT}, - ], zcl.clusters.measurement.RelativeHumidity.cluster_id: [ { "attr": "measured_value", @@ -203,25 +176,9 @@ def establish_device_mappings(): zcl.clusters.smartenergy.Metering.cluster_id: [ {"attr": "instantaneous_demand", "config": REPORT_CONFIG_DEFAULT} ], - zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: [ - {"attr": "active_power", "config": REPORT_CONFIG_DEFAULT} - ], - zcl.clusters.general.PowerConfiguration.cluster_id: [ - {"attr": "battery_voltage", "config": REPORT_CONFIG_DEFAULT}, - { - "attr": "battery_percentage_remaining", - "config": REPORT_CONFIG_DEFAULT, - }, - ], zcl.clusters.measurement.OccupancySensing.cluster_id: [ {"attr": "occupancy", "config": REPORT_CONFIG_IMMEDIATE} ], - zcl.clusters.hvac.Fan.cluster_id: [ - {"attr": "fan_mode", "config": REPORT_CONFIG_OP} - ], - zcl.clusters.closures.DoorLock.cluster_id: [ - {"attr": "lock_state", "config": REPORT_CONFIG_IMMEDIATE} - ], } ) @@ -260,8 +217,6 @@ def establish_device_mappings(): EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id) EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id) - INPUT_BIND_ONLY_CLUSTERS.append(zcl.clusters.lightlink.LightLink.cluster_id) - OUTPUT_CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.general.Scenes.cluster_id) LIGHT_CLUSTERS.add(zcl.clusters.general.LevelControl.cluster_id) diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index abde126e675..dd5cade737c 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -68,12 +68,13 @@ class FakeEndpoint: def patch_cluster(cluster): """Patch a cluster for testing.""" cluster.bind = CoroutineMock(return_value=[0]) + cluster.configure_reporting = CoroutineMock(return_value=[0]) cluster.deserialize = Mock() cluster.handle_cluster_request = Mock() cluster.handle_cluster_general_request = Mock() + cluster.read_attributes = CoroutineMock() cluster.read_attributes_raw = Mock() - cluster.read_attributes = Mock() - cluster.unbind = Mock() + cluster.unbind = CoroutineMock(return_value=[0]) class FakeDevice: diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py new file mode 100644 index 00000000000..1d86a8b0773 --- /dev/null +++ b/tests/components/zha/test_channels.py @@ -0,0 +1,136 @@ +"""Test ZHA Core channels.""" +import pytest +import zigpy.types as t + +import homeassistant.components.zha.core.channels as channels +import homeassistant.components.zha.core.channels.registry as channel_reg +import homeassistant.components.zha.core.device as zha_device + +from .common import make_device + + +@pytest.fixture +def ieee(): + """IEEE fixture.""" + return t.EUI64.deserialize(b"ieeeaddr")[0] + + +@pytest.fixture +def nwk(): + """NWK fixture.""" + return t.NWK(0xBEEF) + + +@pytest.mark.parametrize( + "cluster_id, bind_count, attrs", + [ + (0x0000, 1, {}), + (0x0001, 1, {"battery_voltage", "battery_percentage_remaining"}), + (0x0003, 1, {}), + (0x0004, 1, {}), + (0x0005, 1, {}), + (0x0006, 1, {"on_off"}), + (0x0007, 1, {}), + (0x0008, 1, {"current_level"}), + (0x0009, 1, {}), + (0x0015, 1, {}), + (0x0016, 1, {}), + (0x0019, 1, {}), + (0x001A, 1, {}), + (0x001B, 1, {}), + (0x0020, 1, {}), + (0x0021, 1, {}), + (0x0101, 1, {"lock_state"}), + (0x0202, 1, {"fan_mode"}), + (0x0300, 1, {"current_x", "current_y", "color_temperature"}), + (0x0400, 1, {"measured_value"}), + (0x0402, 1, {"measured_value"}), + (0x0403, 1, {"measured_value"}), + (0x0405, 1, {"measured_value"}), + (0x0406, 1, {"occupancy"}), + (0x0702, 1, {"instantaneous_demand"}), + (0x0B04, 1, {"active_power"}), + (0x1000, 1, {}), + ], +) +async def test_in_channel_config(cluster_id, bind_count, attrs, zha_gateway, hass): + """Test ZHA core channel configuration for input clusters.""" + zigpy_dev = make_device( + [cluster_id], + [], + 0x1234, + "00:11:22:33:44:55:66:77", + "test manufacturer", + "test model", + ) + zha_dev = zha_device.ZHADevice(hass, zigpy_dev, zha_gateway) + + cluster = zigpy_dev.endpoints[1].in_clusters[cluster_id] + channel_class = channel_reg.ZIGBEE_CHANNEL_REGISTRY.get( + cluster_id, channels.AttributeListeningChannel + ) + channel = channel_class(cluster, zha_dev) + + await channel.async_configure() + + assert cluster.bind.call_count == bind_count + assert cluster.configure_reporting.call_count == len(attrs) + reported_attrs = {attr[0][0] for attr in cluster.configure_reporting.call_args_list} + assert set(attrs) == reported_attrs + + +@pytest.mark.parametrize( + "cluster_id, bind_count", + [ + (0x0000, 1), + (0x0001, 1), + (0x0003, 1), + (0x0004, 1), + (0x0005, 1), + (0x0006, 1), + (0x0007, 1), + (0x0008, 1), + (0x0009, 1), + (0x0015, 1), + (0x0016, 1), + (0x0019, 1), + (0x001A, 1), + (0x001B, 1), + (0x0020, 1), + (0x0021, 1), + (0x0101, 1), + (0x0202, 1), + (0x0300, 1), + (0x0400, 1), + (0x0402, 1), + (0x0403, 1), + (0x0405, 1), + (0x0406, 1), + (0x0702, 1), + (0x0B04, 1), + (0x1000, 1), + ], +) +async def test_out_channel_config(cluster_id, bind_count, zha_gateway, hass): + """Test ZHA core channel configuration for output clusters.""" + zigpy_dev = make_device( + [], + [cluster_id], + 0x1234, + "00:11:22:33:44:55:66:77", + "test manufacturer", + "test model", + ) + zha_dev = zha_device.ZHADevice(hass, zigpy_dev, zha_gateway) + + cluster = zigpy_dev.endpoints[1].out_clusters[cluster_id] + cluster.bind_only = True + channel_class = channel_reg.ZIGBEE_CHANNEL_REGISTRY.get( + cluster_id, channels.AttributeListeningChannel + ) + channel = channel_class(cluster, zha_dev) + + await channel.async_configure() + + assert cluster.bind.call_count == bind_count + assert cluster.configure_reporting.call_count == 0 From 70dfe42adb293ca9282bd64dba802a01d46967e7 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sun, 4 Aug 2019 16:03:37 -0700 Subject: [PATCH 045/273] Make myself the codeowner for androidtv (#25697) --- CODEOWNERS | 1 + homeassistant/components/androidtv/manifest.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 3195ca3ac98..65a12177c07 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -24,6 +24,7 @@ homeassistant/components/alpha_vantage/* @fabaff homeassistant/components/amazon_polly/* @robbiet480 homeassistant/components/ambiclimate/* @danielhiversen homeassistant/components/ambient_station/* @bachya +homeassistant/components/androidtv/* @JeffLIrion homeassistant/components/apache_kafka/* @bachya homeassistant/components/api/* @home-assistant/core homeassistant/components/aprs/* @PhilRW diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 9f1233179e7..797ca35a378 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -6,5 +6,5 @@ "androidtv==0.0.18" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@JeffLIrion"] } From 0a87a4bfda36e7d53528a8cdeaabfb40bddcc564 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Sun, 4 Aug 2019 19:21:37 -0400 Subject: [PATCH 046/273] Add usb_path to Z-Wave network_status websocket response (#25617) * Add usb stick path to zwave network_status websocket response * Move to separate websocket command * Return additional config options * add tests --- .../components/zwave/websocket_api.py | 26 ++++++++++++- tests/components/zwave/test_websocket_api.py | 38 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 tests/components/zwave/test_websocket_api.py diff --git a/homeassistant/components/zwave/websocket_api.py b/homeassistant/components/zwave/websocket_api.py index 794d901cce3..7454f2e2c6a 100644 --- a/homeassistant/components/zwave/websocket_api.py +++ b/homeassistant/components/zwave/websocket_api.py @@ -7,7 +7,14 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.core import callback -from .const import DATA_NETWORK +from .const import ( + CONF_AUTOHEAL, + CONF_DEBUG, + CONF_POLLING_INTERVAL, + CONF_USB_STICK_PATH, + DATA_NETWORK, + DATA_ZWAVE_CONFIG, +) _LOGGER = logging.getLogger(__name__) @@ -23,7 +30,24 @@ def websocket_network_status(hass, connection, msg): connection.send_result(msg[ID], {"state": network.state}) +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required(TYPE): "zwave/get_config"}) +def websocket_get_config(hass, connection, msg): + """Get Z-Wave configuration.""" + config = hass.data[DATA_ZWAVE_CONFIG] + connection.send_result( + msg[ID], + { + CONF_AUTOHEAL: config[CONF_AUTOHEAL], + CONF_DEBUG: config[CONF_DEBUG], + CONF_POLLING_INTERVAL: config[CONF_POLLING_INTERVAL], + CONF_USB_STICK_PATH: config[CONF_USB_STICK_PATH], + }, + ) + + @callback def async_load_websocket_api(hass): """Set up the web socket API.""" websocket_api.async_register_command(hass, websocket_network_status) + websocket_api.async_register_command(hass, websocket_get_config) diff --git a/tests/components/zwave/test_websocket_api.py b/tests/components/zwave/test_websocket_api.py new file mode 100644 index 00000000000..b55024eb3f0 --- /dev/null +++ b/tests/components/zwave/test_websocket_api.py @@ -0,0 +1,38 @@ +"""Test Z-Wave Websocket API.""" +from homeassistant.bootstrap import async_setup_component + +from homeassistant.components.zwave.const import ( + CONF_USB_STICK_PATH, + CONF_AUTOHEAL, + CONF_POLLING_INTERVAL, +) +from homeassistant.components.zwave.websocket_api import ID, TYPE + + +async def test_zwave_ws_api(hass, mock_openzwave, hass_ws_client): + """Test Z-Wave websocket API.""" + + await async_setup_component( + hass, + "zwave", + { + "zwave": { + CONF_AUTOHEAL: False, + CONF_USB_STICK_PATH: "/dev/zwave", + CONF_POLLING_INTERVAL: 6000, + } + }, + ) + + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + await client.send_json({ID: 5, TYPE: "zwave/get_config"}) + + msg = await client.receive_json() + result = msg["result"] + + assert result[CONF_USB_STICK_PATH] == "/dev/zwave" + assert not result[CONF_AUTOHEAL] + assert result[CONF_POLLING_INTERVAL] == 6000 From 069c1863eb0a25a0efc2f4b1a9fd2a93dd095ead Mon Sep 17 00:00:00 2001 From: ktnrg45 <38207570+ktnrg45@users.noreply.github.com> Date: Sun, 4 Aug 2019 21:01:40 -0700 Subject: [PATCH 047/273] PS4 Merge async_setup_platform into async_setup_entry (#25689) * Merge async_setup_platform into async_setup_entry * Pass async_setup_platform * blank line * add test setup platform * white space --- homeassistant/components/ps4/media_player.py | 10 +++++----- tests/components/ps4/test_media_player.py | 5 +++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 9c5dd4f73b3..e1ec32ddd1f 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -58,11 +58,6 @@ DEFAULT_RETRIES = 2 async def async_setup_entry(hass, config_entry, async_add_entities): """Set up PS4 from a config entry.""" config = config_entry - await async_setup_platform(hass, config, async_add_entities, discovery_info=None) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up PS4 Platform.""" creds = config.data[CONF_TOKEN] device_list = [] for device in config.data["devices"]: @@ -74,6 +69,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(device_list, update_before_add=True) +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Not Implemented.""" + pass + + class PS4Device(MediaPlayerDevice): """Representation of a PS4.""" diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index 145fd9e3e11..b7f4ff53ec8 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -169,6 +169,11 @@ async def mock_ddp_response(hass, mock_status_data, games=None): await hass.async_block_till_done() +async def test_async_setup_platform_does_nothing(): + """Test setup platform does nothing (Uses config entries only).""" + await ps4.media_player.async_setup_platform(None, None, None) + + async def test_media_player_is_setup_correctly_with_entry(hass): """Test entity is setup correctly with entry correctly.""" mock_entity_id = await setup_mock_component(hass) From 6b511789d67d515376ac64b4e9590989bd456d89 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 4 Aug 2019 22:30:31 -0700 Subject: [PATCH 048/273] Updated frontend to 20190804.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index b6a996afc98..4cea624b4cd 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==20190801.0" + "home-assistant-frontend==20190804.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 942cdb577f9..13c5b1a0144 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2019.6.16 cryptography==2.7 distro==1.4.0 hass-nabucasa==0.16 -home-assistant-frontend==20190801.0 +home-assistant-frontend==20190804.0 importlib-metadata==0.18 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 850c98c71b2..412e15b825b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -626,7 +626,7 @@ hole==0.3.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190801.0 +home-assistant-frontend==20190804.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e92cf8a24bd..23e6896d890 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -174,7 +174,7 @@ hdate==0.8.8 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190801.0 +home-assistant-frontend==20190804.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 57f84cbbaa6945b4326dc1e9f0b70870325c714e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 4 Aug 2019 23:24:54 -0700 Subject: [PATCH 049/273] Update HTTP defaults (#25702) * Update HTTP defaults * Fix tests --- homeassistant/components/http/__init__.py | 4 +++- homeassistant/components/http/cors.py | 3 +++ tests/components/http/test_init.py | 9 +++++++++ tests/scripts/test_check_config.py | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 84c7d15a580..6d31c3fc700 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -51,6 +51,8 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_SERVER_HOST = "0.0.0.0" DEFAULT_DEVELOPMENT = "0" +# To be able to load custom cards. +DEFAULT_CORS = "https://cast.home-assistant.io" NO_LOGIN_ATTEMPT_THRESHOLD = -1 @@ -91,7 +93,7 @@ HTTP_SCHEMA = vol.Schema( vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile, vol.Optional(CONF_SSL_KEY): cv.isfile, - vol.Optional(CONF_CORS_ORIGINS, default=[]): vol.All( + vol.Optional(CONF_CORS_ORIGINS, default=[DEFAULT_CORS]): vol.All( cv.ensure_list, [cv.string] ), vol.Inclusive(CONF_USE_X_FORWARDED_FOR, "proxy"): cv.boolean, diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index 5c24ecbebed..19fe88c5cde 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -45,6 +45,9 @@ def setup_cors(app, origins): path = path.canonical + if path.startswith("/api/hassio_ingress/"): + return + if path in cors_added: return diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index a3837a0b745..d8e613df6df 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -232,3 +232,12 @@ async def test_ssl_profile_change_modern(hass): await hass.async_block_till_done() assert len(mock_context.mock_calls) == 1 + + +async def test_cors_defaults(hass): + """Test the CORS default settings.""" + with patch("homeassistant.components.http.setup_cors") as mock_setup: + assert await async_setup_component(hass, "http", {}) + + assert len(mock_setup.mock_calls) == 1 + assert mock_setup.mock_calls[0][1][1] == ["https://cast.home-assistant.io"] diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 98c634cd400..a07b812bc96 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -103,7 +103,7 @@ def test_secrets(isfile_patch, loop): assert res["components"].keys() == {"homeassistant", "http"} assert res["components"]["http"] == { "api_password": "abc123", - "cors_allowed_origins": [], + "cors_allowed_origins": ["https://cast.home-assistant.io"], "ip_ban_enabled": True, "login_attempts_threshold": -1, "server_host": "0.0.0.0", From cecfb2d65751336f619d24fbade6ba0d4beefd22 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 5 Aug 2019 08:54:44 +0200 Subject: [PATCH 050/273] Update azure-pipelines-wheels.yml for Azure Pipelines --- azure-pipelines-wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index d467cf970b8..ea43d34f746 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -10,8 +10,8 @@ trigger: - requirements_all.txt pr: none schedules: -- cron: "0 */8 * * *" - displayName: daily builds +- cron: '0 */8 * * *' + displayName: 'daily builds' branches: include: - dev From 5e00b546eb70a0407318e7a845c297331c9255fc Mon Sep 17 00:00:00 2001 From: jaminh Date: Mon, 5 Aug 2019 05:25:48 -0500 Subject: [PATCH 051/273] Zwave Climate: Fan state attribute missing (#25287) (#25573) Add fan state back in to device state attributes Change fan state to fan action --- homeassistant/components/zwave/climate.py | 16 ++++++++++++---- homeassistant/components/zwave/const.py | 2 +- .../components/zwave/discovery_schemas.py | 4 ++-- tests/components/zwave/test_climate.py | 18 +++++++++++++----- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index 6f66c6f36c4..a9dfe4eec00 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -39,7 +39,7 @@ REMOTEC = 0x5254 REMOTEC_ZXT_120 = 0x8377 REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120) ATTR_OPERATING_STATE = "operating_state" -ATTR_FAN_STATE = "fan_state" +ATTR_FAN_ACTION = "fan_action" # Device is in manufacturer specific mode (e.g. setting the valve manually) @@ -133,7 +133,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): self._preset_mode = None # ha_mode if exists, else zwave_mode self._current_fan_mode = None self._fan_modes = None - self._fan_state = None + self._fan_action = None self._current_swing_mode = None self._swing_modes = None self._unit = temp_unit @@ -291,8 +291,8 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): self._hvac_action = HVAC_CURRENT_MAPPINGS.get(str(mode).lower(), mode) # Fan operating state - if self.values.fan_state: - self._fan_state = self.values.fan_state.data + if self.values.fan_action: + self._fan_action = self.values.fan_action.data @property def fan_mode(self): @@ -426,3 +426,11 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): if self._zxt_120 == 1: if self.values.zxt_120_swing_mode: self.values.zxt_120_swing_mode.data = swing_mode + + @property + def device_state_attributes(self): + """Return the optional state attributes.""" + data = super().device_state_attributes + if self._fan_action: + data[ATTR_FAN_ACTION] = self._fan_action + return data diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index 72e08299691..d6d1265d09f 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -173,7 +173,7 @@ COMMAND_CLASS_SWITCH_TOGGLE_MULTILEVEL = 41 COMMAND_CLASS_TARIFF_TBL_CONFIG = 74 COMMAND_CLASS_TARIFF_TBL_MONITOR = 75 COMMAND_CLASS_THERMOSTAT_FAN_MODE = 68 -COMMAND_CLASS_THERMOSTAT_FAN_STATE = 69 +COMMAND_CLASS_THERMOSTAT_FAN_ACTION = 69 COMMAND_CLASS_THERMOSTAT_MODE = 64 COMMAND_CLASS_THERMOSTAT_OPERATING_STATE = 66 COMMAND_CLASS_THERMOSTAT_SETBACK = 71 diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py index 1be9ca2834e..dbec1484508 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -78,9 +78,9 @@ DISCOVERY_SCHEMAS = [ ], const.DISC_OPTIONAL: True, }, - "fan_state": { + "fan_action": { const.DISC_COMMAND_CLASS: [ - const.COMMAND_CLASS_THERMOSTAT_FAN_STATE + const.COMMAND_CLASS_THERMOSTAT_FAN_ACTION ], const.DISC_OPTIONAL: True, }, diff --git a/tests/components/zwave/test_climate.py b/tests/components/zwave/test_climate.py index 60a9dcd0dab..2f13d95fb9f 100644 --- a/tests/components/zwave/test_climate.py +++ b/tests/components/zwave/test_climate.py @@ -43,7 +43,7 @@ def device(hass, mock_openzwave): ), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), - fan_state=MockValue(data=7, node=node), + fan_action=MockValue(data=7, node=node), ) device = climate.get_device(hass, node=node, values=values, node_config={}) @@ -70,7 +70,7 @@ def device_zxt_120(hass, mock_openzwave): ), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), - fan_state=MockValue(data=7, node=node), + fan_action=MockValue(data=7, node=node), zxt_120_swing_mode=MockValue(data="test3", data_items=[6, 7, 8], node=node), ) device = climate.get_device(hass, node=node, values=values, node_config={}) @@ -92,7 +92,7 @@ def device_mapping(hass, mock_openzwave): ), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data="heating", node=node), - fan_state=MockValue(data=7, node=node), + fan_action=MockValue(data=7, node=node), ) device = climate.get_device(hass, node=node, values=values, node_config={}) @@ -113,7 +113,7 @@ def device_unknown(hass, mock_openzwave): ), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data="test4", node=node), - fan_state=MockValue(data=7, node=node), + fan_action=MockValue(data=7, node=node), ) device = climate.get_device(hass, node=node, values=values, node_config={}) @@ -140,7 +140,7 @@ def device_heat_cool(hass, mock_openzwave): ), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data="test4", node=node), - fan_state=MockValue(data=7, node=node), + fan_action=MockValue(data=7, node=node), ) device = climate.get_device(hass, node=node, values=values, node_config={}) @@ -442,3 +442,11 @@ def test_hvac_action_value_changed_unknown(device_unknown): device.values.operating_state.data = "another_hvac_action" value_changed(device.values.operating_state) assert device.hvac_action == "another_hvac_action" + + +def test_fan_action_value_changed(device): + """Test values changed for climate device.""" + assert device.device_state_attributes[climate.ATTR_FAN_ACTION] == 7 + device.values.fan_action.data = 9 + value_changed(device.values.fan_action) + assert device.device_state_attributes[climate.ATTR_FAN_ACTION] == 9 From ac5fcff1cd2ac4bc12e4761832ccc63a55e14b73 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 5 Aug 2019 08:47:42 -0400 Subject: [PATCH 052/273] fix unique id in cluster channels (#25707) --- .../components/zha/core/channels/__init__.py | 14 ++++++++++---- homeassistant/components/zha/core/helpers.py | 7 ------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 21bdf8a5575..9a6bf1a3423 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -23,7 +23,7 @@ from ..const import ( REPORT_CONFIG_RPT_CHANGE, SIGNAL_ATTR_UPDATED, ) -from ..helpers import LogMixin, construct_unique_id, get_attr_id_by_name, safe_read +from ..helpers import LogMixin, get_attr_id_by_name, safe_read from ..registries import CLUSTER_REPORT_CONFIGS _LOGGER = logging.getLogger(__name__) @@ -89,7 +89,13 @@ class ZigbeeChannel(LogMixin): self._generic_id = "channel_0x{:04x}".format(cluster.cluster_id) self._cluster = cluster self._zha_device = device - self._unique_id = construct_unique_id(cluster) + self._unique_id = "{}:{}:0x{:04x}".format( + str(device.ieee), cluster.endpoint.endpoint_id, cluster.cluster_id + ) + # this keeps logs consistent with zigpy logging + self._log_id = "0x{:04x}:{}:0x{:04x}".format( + device.nwk, cluster.endpoint.endpoint_id, cluster.cluster_id + ) self._report_config = CLUSTER_REPORT_CONFIGS.get( self._cluster.cluster_id, self.REPORT_CONFIG ) @@ -260,7 +266,7 @@ class ZigbeeChannel(LogMixin): def log(self, level, msg, *args): """Log a message.""" msg = "[%s]: " + msg - args = (self.unique_id,) + args + args = (self._log_id,) + args _LOGGER.log(level, msg, *args) def __getattr__(self, name): @@ -313,7 +319,7 @@ class ZDOChannel(LogMixin): self._cluster = cluster self._zha_device = device self._status = ChannelStatus.CREATED - self._unique_id = "{}_ZDO".format(device.name) + self._unique_id = "{}:{}_ZDO".format(str(device.ieee), device.name) self._cluster.add_listener(self) @property diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 9e7b7efbfd9..7a1791adc56 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -75,13 +75,6 @@ def convert_ieee(ieee_str): return EUI64([uint8_t(p, base=16) for p in ieee_str.split(":")]) -def construct_unique_id(cluster): - """Construct a unique id from a cluster.""" - return "0x{:04x}:{}:0x{:04x}".format( - cluster.endpoint.device.nwk, cluster.endpoint.endpoint_id, cluster.cluster_id - ) - - def get_attr_id_by_name(cluster, attr_name): """Get the attribute id for a cluster attribute by its name.""" return next( From 20b54c22f7338f18e2022667d7776040f3ba3b8e Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 5 Aug 2019 09:50:48 -0400 Subject: [PATCH 053/273] don't mark devices unavailable without ping try (#25710) --- homeassistant/components/zha/core/device.py | 22 ++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 1fe2e1a5e6c..1c22b41ce86 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -39,6 +39,7 @@ from .const import ( ATTR_QUIRK_CLASS, ATTR_RSSI, ATTR_VALUE, + CHANNEL_BASIC, CHANNEL_POWER_CONFIGURATION, CHANNEL_ZDO, CLUSTER_COMMAND_SERVER, @@ -57,6 +58,7 @@ from .helpers import LogMixin _LOGGER = logging.getLogger(__name__) _KEEP_ALIVE_INTERVAL = 7200 _UPDATE_ALIVE_INTERVAL = timedelta(seconds=60) +_CHECKIN_GRACE_PERIODS = 2 class DeviceStatus(Enum): @@ -81,6 +83,7 @@ class ZHADevice(LogMixin): self._available_signal = "{}_{}_{}".format( self.name, self.ieee, SIGNAL_AVAILABLE ) + self._checkins_missed_count = 2 self._unsub = async_dispatcher_connect( self.hass, self._available_signal, self.async_initialize ) @@ -204,9 +207,26 @@ class ZHADevice(LogMixin): else: difference = time.time() - self.last_seen if difference > _KEEP_ALIVE_INTERVAL: - self.update_available(False) + if self._checkins_missed_count < _CHECKIN_GRACE_PERIODS: + self._checkins_missed_count += 1 + if ( + CHANNEL_BASIC in self.cluster_channels + and self.manufacturer != "LUMI" + ): + self.debug( + "Attempting to checkin with device - missed checkins: %s", + self._checkins_missed_count, + ) + self.hass.async_create_task( + self.cluster_channels[CHANNEL_BASIC].get_attribute_value( + ATTR_MANUFACTURER, from_cache=False + ) + ) + else: + self.update_available(False) else: self.update_available(True) + self._checkins_missed_count = 0 def update_available(self, available): """Set sensor availability.""" From b073d87e0842e69e2b1198b37d2a6eddfc94f212 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 5 Aug 2019 10:14:19 -0400 Subject: [PATCH 054/273] stagger device init to avoid flooding network (#25709) --- homeassistant/components/zha/core/gateway.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 5ac02293107..7eff3ebcf3b 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -126,10 +126,21 @@ class ZHAGateway: ) init_tasks = [] + semaphore = asyncio.Semaphore(2) + + async def init_with_semaphore(coro, semaphore): + """Don't flood the zigbee network during initialization.""" + async with semaphore: + await coro + for device in self.application_controller.devices.values(): if device.nwk == 0x0000: continue - init_tasks.append(self.async_device_initialized(device, False)) + init_tasks.append( + init_with_semaphore( + self.async_device_initialized(device, False), semaphore + ) + ) await asyncio.gather(*init_tasks) def device_joined(self, device): From 0449132c3556108a8c529addae2e52c2eaef5e08 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Mon, 5 Aug 2019 09:58:42 -0700 Subject: [PATCH 055/273] Bump androidtv to 0.0.21; add 'state_detection_rules' config parameter (#25647) * Bump androidtv to 0.0.19; add 'state_detection_rules' config parameter * Bump androidtv to 0.0.20 * Add detailed config check for 'state_detection_rules' * Linting * Remove unused variable * Bump androidtv to 0.0.21, move config validation into backend package * dict() -> {} Co-Authored-By: Martin Hjelmare --- .../components/androidtv/manifest.json | 2 +- .../components/androidtv/media_player.py | 25 +++++++++++++------ requirements_all.txt | 2 +- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 797ca35a378..1820c5123e4 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,7 +3,7 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/components/androidtv", "requirements": [ - "androidtv==0.0.18" + "androidtv==0.0.21" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 381f0bb7cf1..96d0a2ce00c 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -3,6 +3,9 @@ import functools import logging import voluptuous as vol +from androidtv import setup, ha_state_detection_rules_validator +from androidtv.constants import APPS, KEYS + from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, @@ -64,6 +67,7 @@ CONF_ADB_SERVER_IP = "adb_server_ip" CONF_ADB_SERVER_PORT = "adb_server_port" CONF_APPS = "apps" CONF_GET_SOURCES = "get_sources" +CONF_STATE_DETECTION_RULES = "state_detection_rules" CONF_TURN_ON_COMMAND = "turn_on_command" CONF_TURN_OFF_COMMAND = "turn_off_command" @@ -99,6 +103,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_APPS, default=dict()): vol.Schema({cv.string: cv.string}), vol.Optional(CONF_TURN_ON_COMMAND): cv.string, vol.Optional(CONF_TURN_OFF_COMMAND): cv.string, + vol.Optional(CONF_STATE_DETECTION_RULES, default={}): vol.Schema( + {cv.string: ha_state_detection_rules_validator(vol.Invalid)} + ), } ) @@ -114,8 +121,6 @@ ANDROIDTV_STATES = { def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Android TV / Fire TV platform.""" - from androidtv import setup - hass.data.setdefault(ANDROIDTV_DOMAIN, {}) host = "{0}:{1}".format(config[CONF_HOST], config[CONF_PORT]) @@ -125,12 +130,19 @@ def setup_platform(hass, config, add_entities, discovery_info=None): adb_log = "using Python ADB implementation " if CONF_ADBKEY in config: aftv = setup( - host, config[CONF_ADBKEY], device_class=config[CONF_DEVICE_CLASS] + host, + config[CONF_ADBKEY], + device_class=config[CONF_DEVICE_CLASS], + state_detection_rules=config[CONF_STATE_DETECTION_RULES], ) adb_log += "with adbkey='{0}'".format(config[CONF_ADBKEY]) else: - aftv = setup(host, device_class=config[CONF_DEVICE_CLASS]) + aftv = setup( + host, + device_class=config[CONF_DEVICE_CLASS], + state_detection_rules=config[CONF_STATE_DETECTION_RULES], + ) adb_log += "without adbkey authentication" else: # Use "pure-python-adb" (communicate with ADB server) @@ -139,6 +151,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): adb_server_ip=config[CONF_ADB_SERVER_IP], adb_server_port=config[CONF_ADB_SERVER_PORT], device_class=config[CONF_DEVICE_CLASS], + state_detection_rules=config[CONF_STATE_DETECTION_RULES], ) adb_log = "using ADB server at {0}:{1}".format( config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT] @@ -251,11 +264,9 @@ class ADBDevice(MediaPlayerDevice): def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): """Initialize the Android TV / Fire TV device.""" - from androidtv.constants import APPS, KEYS - self.aftv = aftv self._name = name - self._apps = APPS + self._apps = APPS.copy() self._apps.update(apps) self._keys = KEYS diff --git a/requirements_all.txt b/requirements_all.txt index 412e15b825b..4062cdc501f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -190,7 +190,7 @@ ambiclimate==0.2.0 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.18 +androidtv==0.0.21 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 From 7a90808e528bea9322a7fd8d7723837fb80cb895 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 5 Aug 2019 14:04:20 -0700 Subject: [PATCH 056/273] Add service to reload scenes from configuration.yaml (#25680) * Allow reloading scenes * Update requirements * address comments * fix typing * fix tests * Update homeassistant/components/homeassistant/scene.py Co-Authored-By: Martin Hjelmare * Address comments --- .../components/homeassistant/scene.py | 68 ++++++++++++++++--- homeassistant/components/scene/__init__.py | 5 ++ homeassistant/helpers/entity_component.py | 9 ++- homeassistant/helpers/entity_platform.py | 8 +++ homeassistant/package_constraints.txt | 1 + requirements_all.txt | 1 + setup.py | 1 + tests/components/homeassistant/test_scene.py | 30 ++++++++ tests/helpers/test_entity_component.py | 2 +- 9 files changed, 113 insertions(+), 12 deletions(-) create mode 100644 tests/components/homeassistant/test_scene.py diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index de8a4dc88e7..66b04109640 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -1,5 +1,6 @@ """Allow users to set and activate scenes.""" from collections import namedtuple +import logging import voluptuous as vol @@ -11,12 +12,19 @@ from homeassistant.const import ( CONF_PLATFORM, STATE_OFF, STATE_ON, + SERVICE_RELOAD, +) +from homeassistant.core import State, DOMAIN +from homeassistant import config as conf_util +from homeassistant.exceptions import HomeAssistantError +from homeassistant.loader import async_get_integration +from homeassistant.helpers import ( + config_per_platform, + config_validation as cv, + entity_platform, ) -from homeassistant.core import State -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.state import HASS_DOMAIN, async_reproduce_state -from homeassistant.components.scene import STATES, Scene - +from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, STATES, Scene PLATFORM_SCHEMA = vol.Schema( { @@ -37,19 +45,63 @@ PLATFORM_SCHEMA = vol.Schema( ) SCENECONFIG = namedtuple("SceneConfig", [CONF_NAME, STATES]) +_LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up home assistant scene entries.""" - scene_config = config.get(STATES) + _process_scenes_config(hass, async_add_entities, config) + + # This platform can be loaded multiple times. Only first time register the service. + if hass.services.has_service(SCENE_DOMAIN, SERVICE_RELOAD): + return + + # Store platform for later. + platform = entity_platform.current_platform.get() + + async def reload_config(call): + """Reload the scene config.""" + try: + conf = await conf_util.async_hass_config_yaml(hass) + except HomeAssistantError as err: + _LOGGER.error(err) + return + + integration = await async_get_integration(hass, SCENE_DOMAIN) + + conf = await conf_util.async_process_component_config(hass, conf, integration) + + if not conf or not platform: + return + + await platform.async_reset() + + # Extract only the config for the Home Assistant platform, ignore the rest. + for p_type, p_config in config_per_platform(conf, SCENE_DOMAIN): + if p_type != DOMAIN: + continue + + _process_scenes_config(hass, async_add_entities, p_config) + + hass.helpers.service.async_register_admin_service( + SCENE_DOMAIN, SERVICE_RELOAD, reload_config + ) + + +def _process_scenes_config(hass, async_add_entities, config): + """Process multiple scenes and add them.""" + scene_config = config[STATES] + + # Check empty list + if not scene_config: + return async_add_entities( - HomeAssistantScene(hass, _process_config(scene)) for scene in scene_config + HomeAssistantScene(hass, _process_scene_config(scene)) for scene in scene_config ) - return True -def _process_config(scene_config): +def _process_scene_config(scene_config): """Process passed in config into a format to work with. Async friendly. diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 0d00c2c5ea2..5ddb1116d8f 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -5,6 +5,7 @@ import logging import voluptuous as vol +from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity import Entity @@ -60,6 +61,10 @@ async def async_setup(hass, config): component = hass.data[DOMAIN] = EntityComponent(logger, DOMAIN, hass) await component.async_setup(config) + # Ensure Home Assistant platform always loaded. + await component.async_setup_platform( + HA_DOMAIN, {"platform": "homeasistant", STATES: []} + ) async def async_handle_scene_service(service): """Handle calls to the switch services.""" diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index ed1b41a0abd..b28beeaea72 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -114,7 +114,7 @@ class EntityComponent: # Look in config for Domain, Domain 2, Domain 3 etc and load them tasks = [] for p_type, p_config in config_per_platform(config, self.domain): - tasks.append(self._async_setup_platform(p_type, p_config)) + tasks.append(self.async_setup_platform(p_type, p_config)) if tasks: await asyncio.wait(tasks) @@ -123,7 +123,7 @@ class EntityComponent: # Refer to: homeassistant.components.discovery.load_platform() async def component_platform_discovered(platform, info): """Handle the loading of a platform.""" - await self._async_setup_platform(platform, {}, info) + await self.async_setup_platform(platform, {}, info) discovery.async_listen_platform( self.hass, self.domain, component_platform_discovered @@ -212,10 +212,13 @@ class EntityComponent: self.hass.services.async_register(self.domain, name, handle_service, schema) - async def _async_setup_platform( + async def async_setup_platform( self, platform_type, platform_config, discovery_info=None ): """Set up a platform for this component.""" + if self.config is None: + raise RuntimeError("async_setup needs to be called first") + platform = await async_prepare_setup_platform( self.hass, self.config, self.domain, platform_type ) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 5012f578106..ea71828f21a 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -1,5 +1,7 @@ """Class to manage the entities for a single platform.""" import asyncio +from contextvars import ContextVar +from typing import Optional from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import callback, valid_entity_id, split_entity_id @@ -127,6 +129,7 @@ class EntityPlatform: async_create_setup_task creates a coroutine that sets up platform. """ + current_platform.set(self) logger = self.logger hass = self.hass full_name = "{}.{}".format(self.domain, self.platform_name) @@ -457,3 +460,8 @@ class EntityPlatform: if tasks: await asyncio.wait(tasks) + + +current_platform: ContextVar[Optional[EntityPlatform]] = ContextVar( + "current_platform", default=None +) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 13c5b1a0144..89465568c65 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -7,6 +7,7 @@ async_timeout==3.0.1 attrs==19.1.0 bcrypt==3.1.7 certifi>=2019.6.16 +contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.16 diff --git a/requirements_all.txt b/requirements_all.txt index 4062cdc501f..e3ff35ad977 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.7 certifi>=2019.6.16 +contextvars==2.4;python_version<"3.7" importlib-metadata==0.18 jinja2>=2.10.1 PyJWT==1.7.1 diff --git a/setup.py b/setup.py index 5133ce9c16b..da50b5f988c 100755 --- a/setup.py +++ b/setup.py @@ -37,6 +37,7 @@ REQUIRES = [ "attrs==19.1.0", "bcrypt==3.1.7", "certifi>=2019.6.16", + 'contextvars==2.4;python_version<"3.7"', "importlib-metadata==0.18", "jinja2>=2.10.1", "PyJWT==1.7.1", diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py new file mode 100644 index 00000000000..02c018a0b49 --- /dev/null +++ b/tests/components/homeassistant/test_scene.py @@ -0,0 +1,30 @@ +"""Test Home Assistant scenes.""" +from unittest.mock import patch + +from homeassistant.setup import async_setup_component + + +async def test_reload_config_service(hass): + """Test the reload config service.""" + assert await async_setup_component(hass, "scene", {}) + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={"scene": {"name": "Hallo", "entities": {"light.kitchen": "on"}}}, + ), patch("homeassistant.config.find_config_file", return_value=""): + await hass.services.async_call("scene", "reload", blocking=True) + await hass.async_block_till_done() + + assert hass.states.get("scene.hallo") is not None + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={"scene": {"name": "Bye", "entities": {"light.kitchen": "on"}}}, + ), patch("homeassistant.config.find_config_file", return_value=""): + await hass.services.async_call("scene", "reload", blocking=True) + await hass.async_block_till_done() + + assert hass.states.get("scene.hallo") is None + assert hass.states.get("scene.bye") is not None diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 3dd6ca8b55f..0d52f430ff5 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -116,7 +116,7 @@ async def test_setup_recovers_when_setup_raises(hass): @asynctest.patch( - "homeassistant.helpers.entity_component.EntityComponent" "._async_setup_platform", + "homeassistant.helpers.entity_component.EntityComponent" ".async_setup_platform", return_value=mock_coro(), ) @asynctest.patch( From 8241193fa8176f3b9de27f0e19c814fafe29a8d6 Mon Sep 17 00:00:00 2001 From: Jesse Rizzo <32472573+jesserizzo@users.noreply.github.com> Date: Mon, 5 Aug 2019 16:15:42 -0500 Subject: [PATCH 057/273] Bump envoy_reader to 0.8.6, fix missing dependency (#25679) * Bump envoy_reader to 0.8.6, fix missing dependency * Bump envoy_reader to 0.8.6, fix missing dependency --- 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 60f252c59a6..86d2d69cf9b 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.8" + "envoy_reader==0.8.6" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index e3ff35ad977..402bba48314 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -443,7 +443,7 @@ env_canada==0.0.20 # envirophat==0.0.6 # homeassistant.components.enphase_envoy -envoy_reader==0.8 +envoy_reader==0.8.6 # homeassistant.components.season ephem==3.7.6.0 From 8dbac9176ef905ea3d4f2252edc487290e862866 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 5 Aug 2019 17:40:29 -0400 Subject: [PATCH 058/273] Refactor ZHA Zigbee channel registry. (#25716) * Update test to catch regression. * Refactor ZHA Core channels. Use channel decorator for Zigbee channel registry. * Update tests. * Pylint --- homeassistant/components/zha/__init__.py | 2 - .../components/zha/core/channels/__init__.py | 19 + .../components/zha/core/channels/closures.py | 19 +- .../components/zha/core/channels/general.py | 340 +++++++++++++----- .../zha/core/channels/homeautomation.py | 40 ++- .../components/zha/core/channels/hvac.py | 33 +- .../components/zha/core/channels/lighting.py | 12 +- .../components/zha/core/channels/lightlink.py | 11 + .../zha/core/channels/measurement.py | 69 ++++ .../components/zha/core/channels/protocol.py | 144 ++++++++ .../components/zha/core/channels/registry.py | 52 --- .../components/zha/core/channels/security.py | 19 +- .../zha/core/channels/smartenergy.py | 89 +++++ .../components/zha/core/discovery.py | 8 +- .../components/zha/core/registries.py | 26 -- tests/components/zha/conftest.py | 4 - tests/components/zha/test_channels.py | 25 +- 17 files changed, 738 insertions(+), 174 deletions(-) delete mode 100644 homeassistant/components/zha/core/channels/registry.py diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index d71362ac1ac..ab686a97989 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -11,7 +11,6 @@ from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from . import config_flow # noqa # pylint: disable=unused-import from . import api from .core import ZHAGateway -from .core.channels.registry import populate_channel_registry from .core.const import ( COMPONENTS, CONF_BAUDRATE, @@ -90,7 +89,6 @@ async def async_setup_entry(hass, config_entry): Will automatically load components to support devices found on the network. """ establish_device_mappings() - populate_channel_registry() for component in COMPONENTS: hass.data[DATA_ZHA][component] = hass.data[DATA_ZHA].get(component, {}) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 9a6bf1a3423..246c17d83d6 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -13,11 +13,13 @@ from random import uniform from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.util.decorator import Registry from ..const import ( CHANNEL_ATTRIBUTE, CHANNEL_EVENT_RELAY, CHANNEL_ZDO, + REPORT_CONFIG_DEFAULT, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MIN_INT, REPORT_CONFIG_RPT_CHANGE, @@ -28,6 +30,8 @@ from ..registries import CLUSTER_REPORT_CONFIGS _LOGGER = logging.getLogger(__name__) +ZIGBEE_CHANNEL_REGISTRY = Registry() + def parse_and_log_command(channel, tsn, command_id, args): """Parse and log a zigbee cluster command.""" @@ -282,6 +286,7 @@ class AttributeListeningChannel(ZigbeeChannel): """Channel for attribute reports from the cluster.""" CHANNEL_NAME = CHANNEL_ATTRIBUTE + REPORT_CONFIG = [{"attr": 0, "config": REPORT_CONFIG_DEFAULT}] def __init__(self, cluster, device): """Initialize AttributeListeningChannel.""" @@ -394,3 +399,17 @@ class EventRelayChannel(ZigbeeChannel): self.zha_send_event( self._cluster, self._cluster.server_commands.get(command_id)[0], args ) + + +# pylint: disable=wrong-import-position +from . import closures # noqa +from . import general # noqa +from . import homeautomation # noqa +from . import hvac # noqa +from . import lighting # noqa +from . import lightlink # noqa +from . import manufacturerspecific # noqa +from . import measurement # noqa +from . import protocol # noqa +from . import security # noqa +from . import smartenergy # noqa diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index 87a331984a5..4bf3bb0fe00 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -6,15 +6,18 @@ https://home-assistant.io/components/zha/ """ import logging +import zigpy.zcl.clusters.closures as closures + from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from . import ZigbeeChannel +from . import ZIGBEE_CHANNEL_REGISTRY, ZigbeeChannel from ..const import REPORT_CONFIG_IMMEDIATE, SIGNAL_ATTR_UPDATED _LOGGER = logging.getLogger(__name__) +@ZIGBEE_CHANNEL_REGISTRY.register(closures.DoorLock.cluster_id) class DoorLockChannel(ZigbeeChannel): """Door lock channel.""" @@ -49,3 +52,17 @@ class DoorLockChannel(ZigbeeChannel): """Initialize channel.""" await self.get_attribute_value(self._value_attribute, from_cache=from_cache) await super().async_initialize(from_cache) + + +@ZIGBEE_CHANNEL_REGISTRY.register(closures.Shade.cluster_id) +class Shade(ZigbeeChannel): + """Shade channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(closures.WindowCovering.cluster_id) +class WindowCovering(ZigbeeChannel): + """Window channel.""" + + pass diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index bc33c2d34f5..c5706c99afd 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -6,24 +6,229 @@ https://home-assistant.io/components/zha/ """ import logging +import zigpy.zcl.clusters.general as general + from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later -from . import ZigbeeChannel, parse_and_log_command +from . import ( + ZIGBEE_CHANNEL_REGISTRY, + AttributeListeningChannel, + ZigbeeChannel, + parse_and_log_command, +) from ..const import ( + REPORT_CONFIG_ASAP, + REPORT_CONFIG_BATTERY_SAVE, + REPORT_CONFIG_DEFAULT, + REPORT_CONFIG_IMMEDIATE, SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL, - REPORT_CONFIG_ASAP, - REPORT_CONFIG_BATTERY_SAVE, - REPORT_CONFIG_IMMEDIATE, ) from ..helpers import get_attr_id_by_name _LOGGER = logging.getLogger(__name__) +@ZIGBEE_CHANNEL_REGISTRY.register(general.Alarms.cluster_id) +class Alarms(ZigbeeChannel): + """Alarms channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogInput.cluster_id) +class AnalogInput(AttributeListeningChannel): + """Analog Input channel.""" + + REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogOutput.cluster_id) +class AnalogOutput(AttributeListeningChannel): + """Analog Output channel.""" + + REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogValue.cluster_id) +class AnalogValue(AttributeListeningChannel): + """Analog Value channel.""" + + REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.ApplianceControl.cluster_id) +class ApplianceContorl(ZigbeeChannel): + """Appliance Control channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.Basic.cluster_id) +class BasicChannel(ZigbeeChannel): + """Channel to interact with the basic cluster.""" + + UNKNOWN = 0 + BATTERY = 3 + + POWER_SOURCES = { + UNKNOWN: "Unknown", + 1: "Mains (single phase)", + 2: "Mains (3 phase)", + BATTERY: "Battery", + 4: "DC source", + 5: "Emergency mains constantly powered", + 6: "Emergency mains and transfer switch", + } + + def __init__(self, cluster, device): + """Initialize BasicChannel.""" + super().__init__(cluster, device) + self._power_source = None + + async def async_configure(self): + """Configure this channel.""" + await super().async_configure() + await self.async_initialize(False) + + async def async_initialize(self, from_cache): + """Initialize channel.""" + self._power_source = await self.get_attribute_value( + "power_source", from_cache=from_cache + ) + await super().async_initialize(from_cache) + + def get_power_source(self): + """Get the power source.""" + return self._power_source + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryInput.cluster_id) +class BinaryInput(AttributeListeningChannel): + """Binary Input channel.""" + + REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryOutput.cluster_id) +class BinaryOutput(AttributeListeningChannel): + """Binary Output channel.""" + + REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryValue.cluster_id) +class BinaryValue(AttributeListeningChannel): + """Binary Value channel.""" + + REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.Commissioning.cluster_id) +class Commissioning(ZigbeeChannel): + """Commissioning channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.DeviceTemperature.cluster_id) +class DeviceTemperature(ZigbeeChannel): + """Device Temperatur channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.GreenPowerProxy.cluster_id) +class GreenPowerProxy(ZigbeeChannel): + """Green Power Proxy channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.Groups.cluster_id) +class Groups(ZigbeeChannel): + """Groups channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.Identify.cluster_id) +class Identify(ZigbeeChannel): + """Identify channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.LevelControl.cluster_id) +class LevelControlChannel(ZigbeeChannel): + """Channel for the LevelControl Zigbee cluster.""" + + CURRENT_LEVEL = 0 + REPORT_CONFIG = ({"attr": "current_level", "config": REPORT_CONFIG_ASAP},) + + @callback + def cluster_command(self, tsn, command_id, args): + """Handle commands received to this cluster.""" + cmd = parse_and_log_command(self, tsn, command_id, args) + + if cmd in ("move_to_level", "move_to_level_with_on_off"): + self.dispatch_level_change(SIGNAL_SET_LEVEL, args[0]) + elif cmd in ("move", "move_with_on_off"): + # We should dim slowly -- for now, just step once + rate = args[1] + if args[0] == 0xFF: + rate = 10 # Should read default move rate + self.dispatch_level_change(SIGNAL_MOVE_LEVEL, -rate if args[0] else rate) + elif cmd in ("step", "step_with_on_off"): + # Step (technically may change on/off) + self.dispatch_level_change( + SIGNAL_MOVE_LEVEL, -args[1] if args[0] else args[1] + ) + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute updates on this cluster.""" + self.debug("received attribute: %s update with value: %s", attrid, value) + if attrid == self.CURRENT_LEVEL: + self.dispatch_level_change(SIGNAL_SET_LEVEL, value) + + def dispatch_level_change(self, command, level): + """Dispatch level change.""" + async_dispatcher_send( + self._zha_device.hass, "{}_{}".format(self.unique_id, command), level + ) + + async def async_initialize(self, from_cache): + """Initialize channel.""" + await self.get_attribute_value(self.CURRENT_LEVEL, from_cache=from_cache) + await super().async_initialize(from_cache) + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateInput.cluster_id) +class MultistateInput(AttributeListeningChannel): + """Multistate Input channel.""" + + REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateOutput.cluster_id) +class MultistateOutput(AttributeListeningChannel): + """Multistate Output channel.""" + + REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateValue.cluster_id) +class MultistateValue(AttributeListeningChannel): + """Multistate Value channel.""" + + REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.OnOff.cluster_id) class OnOffChannel(ZigbeeChannel): """Channel for the OnOff Zigbee cluster.""" @@ -97,88 +302,35 @@ class OnOffChannel(ZigbeeChannel): await super().async_update() -class LevelControlChannel(ZigbeeChannel): - """Channel for the LevelControl Zigbee cluster.""" +@ZIGBEE_CHANNEL_REGISTRY.register(general.OnOffConfiguration.cluster_id) +class OnOffConfiguration(ZigbeeChannel): + """OnOff Configuration channel.""" - CURRENT_LEVEL = 0 - REPORT_CONFIG = ({"attr": "current_level", "config": REPORT_CONFIG_ASAP},) - - @callback - def cluster_command(self, tsn, command_id, args): - """Handle commands received to this cluster.""" - cmd = parse_and_log_command(self, tsn, command_id, args) - - if cmd in ("move_to_level", "move_to_level_with_on_off"): - self.dispatch_level_change(SIGNAL_SET_LEVEL, args[0]) - elif cmd in ("move", "move_with_on_off"): - # We should dim slowly -- for now, just step once - rate = args[1] - if args[0] == 0xFF: - rate = 10 # Should read default move rate - self.dispatch_level_change(SIGNAL_MOVE_LEVEL, -rate if args[0] else rate) - elif cmd in ("step", "step_with_on_off"): - # Step (technically may change on/off) - self.dispatch_level_change( - SIGNAL_MOVE_LEVEL, -args[1] if args[0] else args[1] - ) - - @callback - def attribute_updated(self, attrid, value): - """Handle attribute updates on this cluster.""" - self.debug("received attribute: %s update with value: %s", attrid, value) - if attrid == self.CURRENT_LEVEL: - self.dispatch_level_change(SIGNAL_SET_LEVEL, value) - - def dispatch_level_change(self, command, level): - """Dispatch level change.""" - async_dispatcher_send( - self._zha_device.hass, "{}_{}".format(self.unique_id, command), level - ) - - async def async_initialize(self, from_cache): - """Initialize channel.""" - await self.get_attribute_value(self.CURRENT_LEVEL, from_cache=from_cache) - await super().async_initialize(from_cache) + pass -class BasicChannel(ZigbeeChannel): - """Channel to interact with the basic cluster.""" +@ZIGBEE_CHANNEL_REGISTRY.register(general.Ota.cluster_id) +class Ota(ZigbeeChannel): + """OTA Channel.""" - UNKNOWN = 0 - BATTERY = 3 - - POWER_SOURCES = { - UNKNOWN: "Unknown", - 1: "Mains (single phase)", - 2: "Mains (3 phase)", - BATTERY: "Battery", - 4: "DC source", - 5: "Emergency mains constantly powered", - 6: "Emergency mains and transfer switch", - } - - def __init__(self, cluster, device): - """Initialize BasicChannel.""" - super().__init__(cluster, device) - self._power_source = None - - async def async_configure(self): - """Configure this channel.""" - await super().async_configure() - await self.async_initialize(False) - - async def async_initialize(self, from_cache): - """Initialize channel.""" - self._power_source = await self.get_attribute_value( - "power_source", from_cache=from_cache - ) - await super().async_initialize(from_cache) - - def get_power_source(self): - """Get the power source.""" - return self._power_source + pass +@ZIGBEE_CHANNEL_REGISTRY.register(general.Partition.cluster_id) +class Partition(ZigbeeChannel): + """Partition channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.PollControl.cluster_id) +class PollControl(ZigbeeChannel): + """Poll Control channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.PowerConfiguration.cluster_id) class PowerConfigurationChannel(ZigbeeChannel): """Channel for the zigbee power configuration cluster.""" @@ -219,3 +371,31 @@ class PowerConfigurationChannel(ZigbeeChannel): ) await self.get_attribute_value("battery_voltage", from_cache=from_cache) await self.get_attribute_value("battery_quantity", from_cache=from_cache) + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.PowerProfile.cluster_id) +class PowerProfile(ZigbeeChannel): + """Power Profile channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.RSSILocation.cluster_id) +class RSSILocation(ZigbeeChannel): + """RSSI Location channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.Scenes.cluster_id) +class Scenes(ZigbeeChannel): + """Scenes channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(general.Time.cluster_id) +class Time(ZigbeeChannel): + """Time channel.""" + + pass diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index f0888d29682..e3974d69d84 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -6,9 +6,11 @@ https://home-assistant.io/components/zha/ """ import logging +import zigpy.zcl.clusters.homeautomation as homeautomation + from homeassistant.helpers.dispatcher import async_dispatcher_send -from . import AttributeListeningChannel +from . import ZIGBEE_CHANNEL_REGISTRY, AttributeListeningChannel, ZigbeeChannel from ..const import ( CHANNEL_ELECTRICAL_MEASUREMENT, REPORT_CONFIG_DEFAULT, @@ -18,6 +20,35 @@ from ..const import ( _LOGGER = logging.getLogger(__name__) +@ZIGBEE_CHANNEL_REGISTRY.register(homeautomation.ApplianceEventAlerts.cluster_id) +class ApplianceEventAlerts(ZigbeeChannel): + """Appliance Event Alerts channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(homeautomation.ApplianceIdentification.cluster_id) +class ApplianceIdentification(ZigbeeChannel): + """Appliance Identification channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(homeautomation.ApplianceStatistics.cluster_id) +class ApplianceStatistics(ZigbeeChannel): + """Appliance Statistics channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(homeautomation.Diagnostic.cluster_id) +class Diagnostic(ZigbeeChannel): + """Diagnostic channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(homeautomation.ElectricalMeasurement.cluster_id) class ElectricalMeasurementChannel(AttributeListeningChannel): """Channel that polls active power level.""" @@ -40,3 +71,10 @@ class ElectricalMeasurementChannel(AttributeListeningChannel): """Initialize channel.""" await self.get_attribute_value("active_power", from_cache=from_cache) await super().async_initialize(from_cache) + + +@ZIGBEE_CHANNEL_REGISTRY.register(homeautomation.MeterIdentification.cluster_id) +class MeterIdentification(ZigbeeChannel): + """Metering Identification channel.""" + + pass diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index c9c809ce245..2c115fd1118 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -6,15 +6,25 @@ https://home-assistant.io/components/zha/ """ import logging +import zigpy.zcl.clusters.hvac as hvac + from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from . import ZigbeeChannel +from . import ZIGBEE_CHANNEL_REGISTRY, ZigbeeChannel from ..const import REPORT_CONFIG_OP, SIGNAL_ATTR_UPDATED _LOGGER = logging.getLogger(__name__) +@ZIGBEE_CHANNEL_REGISTRY.register(hvac.Dehumidification.cluster_id) +class Dehumidification(ZigbeeChannel): + """Dehumidification channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(hvac.Fan.cluster_id) class FanChannel(ZigbeeChannel): """Fan channel.""" @@ -59,3 +69,24 @@ class FanChannel(ZigbeeChannel): """Initialize channel.""" await self.get_attribute_value(self._value_attribute, from_cache=from_cache) await super().async_initialize(from_cache) + + +@ZIGBEE_CHANNEL_REGISTRY.register(hvac.Pump.cluster_id) +class Pump(ZigbeeChannel): + """Pump channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(hvac.Thermostat.cluster_id) +class Thermostat(ZigbeeChannel): + """Thermostat channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(hvac.UserInterface.cluster_id) +class UserInterface(ZigbeeChannel): + """User interface (thermostat) channel.""" + + pass diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index 11762c0fe00..6448ea0d163 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -6,12 +6,22 @@ https://home-assistant.io/components/zha/ """ import logging -from . import ZigbeeChannel +import zigpy.zcl.clusters.lighting as lighting + +from . import ZIGBEE_CHANNEL_REGISTRY, ZigbeeChannel from ..const import REPORT_CONFIG_DEFAULT _LOGGER = logging.getLogger(__name__) +@ZIGBEE_CHANNEL_REGISTRY.register(lighting.Ballast.cluster_id) +class Ballast(ZigbeeChannel): + """Ballast channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(lighting.Color.cluster_id) class ColorChannel(ZigbeeChannel): """Color channel.""" diff --git a/homeassistant/components/zha/core/channels/lightlink.py b/homeassistant/components/zha/core/channels/lightlink.py index 83fca6e80c2..a1308160b0f 100644 --- a/homeassistant/components/zha/core/channels/lightlink.py +++ b/homeassistant/components/zha/core/channels/lightlink.py @@ -6,4 +6,15 @@ https://home-assistant.io/components/zha/ """ import logging +import zigpy.zcl.clusters.lightlink as lightlink + +from . import ZIGBEE_CHANNEL_REGISTRY, ZigbeeChannel + _LOGGER = logging.getLogger(__name__) + + +@ZIGBEE_CHANNEL_REGISTRY.register(lightlink.LightLink.cluster_id) +class LightLink(ZigbeeChannel): + """Lightlink channel.""" + + pass diff --git a/homeassistant/components/zha/core/channels/measurement.py b/homeassistant/components/zha/core/channels/measurement.py index 51146289e69..7de9ffdb54c 100644 --- a/homeassistant/components/zha/core/channels/measurement.py +++ b/homeassistant/components/zha/core/channels/measurement.py @@ -6,4 +6,73 @@ https://home-assistant.io/components/zha/ """ import logging +import zigpy.zcl.clusters.measurement as measurement + +from . import ZIGBEE_CHANNEL_REGISTRY, AttributeListeningChannel +from ..const import ( + REPORT_CONFIG_DEFAULT, + REPORT_CONFIG_IMMEDIATE, + REPORT_CONFIG_MAX_INT, + REPORT_CONFIG_MIN_INT, +) + _LOGGER = logging.getLogger(__name__) + + +@ZIGBEE_CHANNEL_REGISTRY.register(measurement.FlowMeasurement.cluster_id) +class FlowMeasurement(AttributeListeningChannel): + """Flow Measurement channel.""" + + REPORT_CONFIG = [{"attr": "measured_value", "config": REPORT_CONFIG_DEFAULT}] + + +@ZIGBEE_CHANNEL_REGISTRY.register(measurement.IlluminanceLevelSensing.cluster_id) +class IlluminanceLevelSensing(AttributeListeningChannel): + """Illuminance Level Sensing channel.""" + + REPORT_CONFIG = [{"attr": "level_status", "config": REPORT_CONFIG_DEFAULT}] + + +@ZIGBEE_CHANNEL_REGISTRY.register(measurement.IlluminanceMeasurement.cluster_id) +class IlluminanceMeasurement(AttributeListeningChannel): + """Illuminance Measurement channel.""" + + REPORT_CONFIG = [{"attr": "measured_value", "config": REPORT_CONFIG_DEFAULT}] + + +@ZIGBEE_CHANNEL_REGISTRY.register(measurement.OccupancySensing.cluster_id) +class OccupancySensing(AttributeListeningChannel): + """Occupancy Sensing channel.""" + + REPORT_CONFIG = [{"attr": "occupancy", "config": REPORT_CONFIG_IMMEDIATE}] + + +@ZIGBEE_CHANNEL_REGISTRY.register(measurement.PressureMeasurement.cluster_id) +class PressureMeasurement(AttributeListeningChannel): + """Pressure measurement channel.""" + + REPORT_CONFIG = [{"attr": "measured_value", "config": REPORT_CONFIG_DEFAULT}] + + +@ZIGBEE_CHANNEL_REGISTRY.register(measurement.RelativeHumidity.cluster_id) +class RelativeHumidity(AttributeListeningChannel): + """Relative Humidity measurement channel.""" + + REPORT_CONFIG = [ + { + "attr": "measured_value", + "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), + } + ] + + +@ZIGBEE_CHANNEL_REGISTRY.register(measurement.TemperatureMeasurement.cluster_id) +class TemperatureMeasurement(AttributeListeningChannel): + """Temperature measurement channel.""" + + REPORT_CONFIG = [ + { + "attr": "measured_value", + "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), + } + ] diff --git a/homeassistant/components/zha/core/channels/protocol.py b/homeassistant/components/zha/core/channels/protocol.py index 2cae156aec5..8918cf90211 100644 --- a/homeassistant/components/zha/core/channels/protocol.py +++ b/homeassistant/components/zha/core/channels/protocol.py @@ -6,4 +6,148 @@ https://home-assistant.io/components/zha/ """ import logging +import zigpy.zcl.clusters.protocol as protocol + +from ..channels import ZIGBEE_CHANNEL_REGISTRY, ZigbeeChannel + _LOGGER = logging.getLogger(__name__) + + +@ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogInputExtended.cluster_id) +class AnalogInputExtended(ZigbeeChannel): + """Analog Input Extended channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogInputRegular.cluster_id) +class AnalogInputRegular(ZigbeeChannel): + """Analog Input Regular channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogOutputExtended.cluster_id) +class AnalogOutputExtended(ZigbeeChannel): + """Analog Output Regular channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogOutputRegular.cluster_id) +class AnalogOutputRegular(ZigbeeChannel): + """Analog Output Regular channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogValueExtended.cluster_id) +class AnalogValueExtended(ZigbeeChannel): + """Analog Value Extended edition channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogValueRegular.cluster_id) +class AnalogValueRegular(ZigbeeChannel): + """Analog Value Regular channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(protocol.BacnetProtocolTunnel.cluster_id) +class BacnetProtocolTunnel(ZigbeeChannel): + """Bacnet Protocol Tunnel channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryInputExtended.cluster_id) +class BinaryInputExtended(ZigbeeChannel): + """Binary Input Extended channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryInputRegular.cluster_id) +class BinaryInputRegular(ZigbeeChannel): + """Binary Input Regular channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryOutputExtended.cluster_id) +class BinaryOutputExtended(ZigbeeChannel): + """Binary Output Extended channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryOutputRegular.cluster_id) +class BinaryOutputRegular(ZigbeeChannel): + """Binary Output Regular channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryValueExtended.cluster_id) +class BinaryValueExtended(ZigbeeChannel): + """Binary Value Extended channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryValueRegular.cluster_id) +class BinaryValueRegular(ZigbeeChannel): + """Binary Value Regular channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(protocol.GenericTunnel.cluster_id) +class GenericTunnel(ZigbeeChannel): + """Generic Tunnel channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(protocol.MultistateInputExtended.cluster_id) +class MultiStateInputExtended(ZigbeeChannel): + """Multistate Input Extended channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(protocol.MultistateInputRegular.cluster_id) +class MultiStateInputRegular(ZigbeeChannel): + """Multistate Input Regular channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(protocol.MultistateOutputExtended.cluster_id) +class MultiStateOutputExtended(ZigbeeChannel): + """Multistate Output Extended channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(protocol.MultistateOutputRegular.cluster_id) +class MultiStateOutputRegular(ZigbeeChannel): + """Multistate Output Regular channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(protocol.MultistateValueExtended.cluster_id) +class MultiStateValueExtended(ZigbeeChannel): + """Multistate Value Extended channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(protocol.MultistateValueRegular.cluster_id) +class MultiStateValueRegular(ZigbeeChannel): + """Multistate Value Regular channel.""" + + pass diff --git a/homeassistant/components/zha/core/channels/registry.py b/homeassistant/components/zha/core/channels/registry.py deleted file mode 100644 index 86527e0ac4a..00000000000 --- a/homeassistant/components/zha/core/channels/registry.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -Channel registry module for Zigbee Home Automation. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ -""" -from . import ZigbeeChannel -from .closures import DoorLockChannel -from .general import ( - BasicChannel, - LevelControlChannel, - OnOffChannel, - PowerConfigurationChannel, -) -from .homeautomation import ElectricalMeasurementChannel -from .hvac import FanChannel -from .lighting import ColorChannel -from .security import IASZoneChannel - -ZIGBEE_CHANNEL_REGISTRY = {} - - -def populate_channel_registry(): - """Populate the channel registry.""" - from zigpy import zcl - - ZIGBEE_CHANNEL_REGISTRY.update( - { - zcl.clusters.closures.DoorLock.cluster_id: DoorLockChannel, - zcl.clusters.general.Alarms.cluster_id: ZigbeeChannel, - zcl.clusters.general.ApplianceControl.cluster_id: ZigbeeChannel, - zcl.clusters.general.Basic.cluster_id: BasicChannel, - zcl.clusters.general.Commissioning.cluster_id: ZigbeeChannel, - zcl.clusters.general.GreenPowerProxy.cluster_id: ZigbeeChannel, - zcl.clusters.general.Groups.cluster_id: ZigbeeChannel, - zcl.clusters.general.Identify.cluster_id: ZigbeeChannel, - zcl.clusters.general.LevelControl.cluster_id: LevelControlChannel, - zcl.clusters.general.OnOff.cluster_id: OnOffChannel, - zcl.clusters.general.OnOffConfiguration.cluster_id: ZigbeeChannel, - zcl.clusters.general.Ota.cluster_id: ZigbeeChannel, - zcl.clusters.general.Partition.cluster_id: ZigbeeChannel, - zcl.clusters.general.PollControl.cluster_id: ZigbeeChannel, - zcl.clusters.general.PowerConfiguration.cluster_id: PowerConfigurationChannel, - zcl.clusters.general.PowerProfile.cluster_id: ZigbeeChannel, - zcl.clusters.general.Scenes.cluster_id: ZigbeeChannel, - zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: ElectricalMeasurementChannel, - zcl.clusters.hvac.Fan.cluster_id: FanChannel, - zcl.clusters.lighting.Color.cluster_id: ColorChannel, - zcl.clusters.lightlink.LightLink.cluster_id: ZigbeeChannel, - zcl.clusters.security.IasZone.cluster_id: IASZoneChannel, - } - ) diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index c7f2366d097..251b16a20df 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -6,15 +6,32 @@ https://home-assistant.io/components/zha/ """ import logging +import zigpy.zcl.clusters.security as security + from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from . import ZigbeeChannel +from . import ZIGBEE_CHANNEL_REGISTRY, ZigbeeChannel from ..const import SIGNAL_ATTR_UPDATED _LOGGER = logging.getLogger(__name__) +@ZIGBEE_CHANNEL_REGISTRY.register(security.IasAce.cluster_id) +class IasAce(ZigbeeChannel): + """IAS Ancillary Control Equipment channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(security.IasWd.cluster_id) +class IasWd(ZigbeeChannel): + """IAS Warning Device channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(security.IasZone.cluster_id) class IASZoneChannel(ZigbeeChannel): """Channel for the IASZone Zigbee cluster.""" diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index d17eae30a96..7ab850da09b 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -6,4 +6,93 @@ https://home-assistant.io/components/zha/ """ import logging +import zigpy.zcl.clusters.smartenergy as smartenergy + +from ..channels import ZIGBEE_CHANNEL_REGISTRY, AttributeListeningChannel, ZigbeeChannel +from ..const import REPORT_CONFIG_DEFAULT + _LOGGER = logging.getLogger(__name__) + + +@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Calendar.cluster_id) +class Calendar(ZigbeeChannel): + """Calendar channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.DeviceManagement.cluster_id) +class DeviceManagement(ZigbeeChannel): + """Device Management channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Drlc.cluster_id) +class Drlc(ZigbeeChannel): + """Demand Response and Load Control channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.EnergyManagement.cluster_id) +class EnergyManagement(ZigbeeChannel): + """Energy Management channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Events.cluster_id) +class Events(ZigbeeChannel): + """Event channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.KeyEstablishment.cluster_id) +class KeyEstablishment(ZigbeeChannel): + """Key Establishment channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.MduPairing.cluster_id) +class MduPairing(ZigbeeChannel): + """Pairing channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Messaging.cluster_id) +class Messaging(ZigbeeChannel): + """Messaging channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Metering.cluster_id) +class Metering(AttributeListeningChannel): + """Metering channel.""" + + REPORT_CONFIG = [{"attr": "instantaneous_demand", "config": REPORT_CONFIG_DEFAULT}] + + +@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Prepayment.cluster_id) +class Prepayment(ZigbeeChannel): + """Prepayment channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Price.cluster_id) +class Price(ZigbeeChannel): + """Price channel.""" + + pass + + +@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Tunneling.cluster_id) +class Tunneling(ZigbeeChannel): + """Tunneling channel.""" + + pass diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 687728b5e26..9936a3451c8 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -13,8 +13,12 @@ from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from .channels import AttributeListeningChannel, EventRelayChannel, ZDOChannel -from .channels.registry import ZIGBEE_CHANNEL_REGISTRY +from .channels import ( + ZIGBEE_CHANNEL_REGISTRY, + AttributeListeningChannel, + EventRelayChannel, + ZDOChannel, +) from .const import ( COMPONENTS, CONF_DEVICE_CONFIG, diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index ec05ec19551..1e1d111fa3e 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -16,8 +16,6 @@ from homeassistant.components.switch import DOMAIN as SWITCH from .const import ( CONTROLLER, REPORT_CONFIG_ASAP, - REPORT_CONFIG_DEFAULT, - REPORT_CONFIG_IMMEDIATE, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MIN_INT, SENSOR_ACCELERATION, @@ -143,18 +141,6 @@ def establish_device_mappings(): CLUSTER_REPORT_CONFIGS.update( { - zcl.clusters.measurement.RelativeHumidity.cluster_id: [ - { - "attr": "measured_value", - "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), - } - ], - zcl.clusters.measurement.TemperatureMeasurement.cluster_id: [ - { - "attr": "measured_value", - "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), - } - ], SMARTTHINGS_ACCELERATION_CLUSTER: [ {"attr": "acceleration", "config": REPORT_CONFIG_ASAP}, {"attr": "x_axis", "config": REPORT_CONFIG_ASAP}, @@ -167,18 +153,6 @@ def establish_device_mappings(): "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), } ], - zcl.clusters.measurement.PressureMeasurement.cluster_id: [ - {"attr": "measured_value", "config": REPORT_CONFIG_DEFAULT} - ], - zcl.clusters.measurement.IlluminanceMeasurement.cluster_id: [ - {"attr": "measured_value", "config": REPORT_CONFIG_DEFAULT} - ], - zcl.clusters.smartenergy.Metering.cluster_id: [ - {"attr": "instantaneous_demand", "config": REPORT_CONFIG_DEFAULT} - ], - zcl.clusters.measurement.OccupancySensing.cluster_id: [ - {"attr": "occupancy", "config": REPORT_CONFIG_IMMEDIATE} - ], } ) diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index c159d2b9486..5433fc62a61 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -6,9 +6,6 @@ from homeassistant.components.zha.core.const import DOMAIN, DATA_ZHA, COMPONENTS from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg from homeassistant.components.zha.core.gateway import ZHAGateway from homeassistant.components.zha.core.registries import establish_device_mappings -from homeassistant.components.zha.core.channels.registry import ( - populate_channel_registry, -) from .common import async_setup_entry from homeassistant.components.zha.core.store import async_get_registry @@ -29,7 +26,6 @@ async def zha_gateway_fixture(hass, config_entry): Create a ZHAGateway object that can be used to interact with as if we had a real zigbee network running. """ - populate_channel_registry() establish_device_mappings() for component in COMPONENTS: hass.data[DATA_ZHA][component] = hass.data[DATA_ZHA].get(component, {}) diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index 1d86a8b0773..e5d50060279 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -1,9 +1,9 @@ """Test ZHA Core channels.""" +import homeassistant.components.zha.core.channels import pytest import zigpy.types as t import homeassistant.components.zha.core.channels as channels -import homeassistant.components.zha.core.channels.registry as channel_reg import homeassistant.components.zha.core.device as zha_device from .common import make_device @@ -33,6 +33,15 @@ def nwk(): (0x0007, 1, {}), (0x0008, 1, {"current_level"}), (0x0009, 1, {}), + (0x000C, 1, {"present_value"}), + (0x000D, 1, {"present_value"}), + (0x000E, 1, {"present_value"}), + (0x000D, 1, {"present_value"}), + (0x0010, 1, {"present_value"}), + (0x0011, 1, {"present_value"}), + (0x0012, 1, {"present_value"}), + (0x0013, 1, {"present_value"}), + (0x0014, 1, {"present_value"}), (0x0015, 1, {}), (0x0016, 1, {}), (0x0019, 1, {}), @@ -44,8 +53,10 @@ def nwk(): (0x0202, 1, {"fan_mode"}), (0x0300, 1, {"current_x", "current_y", "color_temperature"}), (0x0400, 1, {"measured_value"}), + (0x0401, 1, {"level_status"}), (0x0402, 1, {"measured_value"}), (0x0403, 1, {"measured_value"}), + (0x0404, 1, {"measured_value"}), (0x0405, 1, {"measured_value"}), (0x0406, 1, {"occupancy"}), (0x0702, 1, {"instantaneous_demand"}), @@ -66,7 +77,7 @@ async def test_in_channel_config(cluster_id, bind_count, attrs, zha_gateway, has zha_dev = zha_device.ZHADevice(hass, zigpy_dev, zha_gateway) cluster = zigpy_dev.endpoints[1].in_clusters[cluster_id] - channel_class = channel_reg.ZIGBEE_CHANNEL_REGISTRY.get( + channel_class = channels.ZIGBEE_CHANNEL_REGISTRY.get( cluster_id, channels.AttributeListeningChannel ) channel = channel_class(cluster, zha_dev) @@ -125,7 +136,7 @@ async def test_out_channel_config(cluster_id, bind_count, zha_gateway, hass): cluster = zigpy_dev.endpoints[1].out_clusters[cluster_id] cluster.bind_only = True - channel_class = channel_reg.ZIGBEE_CHANNEL_REGISTRY.get( + channel_class = homeassistant.components.zha.core.channels.ZIGBEE_CHANNEL_REGISTRY.get( cluster_id, channels.AttributeListeningChannel ) channel = channel_class(cluster, zha_dev) @@ -134,3 +145,11 @@ async def test_out_channel_config(cluster_id, bind_count, zha_gateway, hass): assert cluster.bind.call_count == bind_count assert cluster.configure_reporting.call_count == 0 + + +def test_channel_registry(): + """Test ZIGBEE Channel Registry.""" + for cluster_id, channel in channels.ZIGBEE_CHANNEL_REGISTRY.items(): + assert isinstance(cluster_id, int) + assert 0 <= cluster_id <= 0xFFFF + assert issubclass(channel, channels.ZigbeeChannel) From 5b0255525579060c2bcb25ca404390667d723b77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Tue, 6 Aug 2019 00:05:07 +0200 Subject: [PATCH 059/273] Add zigate support to zha (#25552) * Add zigpy-zigate support * update requirements * fix * update * fix flake8 * update requirements * fix * update * add test to make codecov happy * fix flake8 * Try to add test * add test * remove unneeded test * exclude registries.py from coverage exclude homeassistant/components/zha/core/registries.py since untestable * Fix merge: black formatting and flake8. --- .coveragerc | 1 + homeassistant/components/zha/core/const.py | 3 ++ homeassistant/components/zha/core/gateway.py | 5 +++ homeassistant/components/zha/core/helpers.py | 5 +++ .../components/zha/core/registries.py | 36 +++++++++++++------ homeassistant/components/zha/manifest.json | 3 +- requirements_all.txt | 3 ++ 7 files changed, 44 insertions(+), 12 deletions(-) diff --git a/.coveragerc b/.coveragerc index abd29a8614b..75b97e8f5e1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -741,6 +741,7 @@ omit = homeassistant/components/zha/core/device.py homeassistant/components/zha/core/gateway.py homeassistant/components/zha/core/helpers.py + homeassistant/components/zha/core/registries.py homeassistant/components/zha/device_entity.py homeassistant/components/zha/entity.py homeassistant/components/zha/light.py diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 7a4f5f94897..c35cb168fdf 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -79,6 +79,7 @@ DEBUG_COMP_ZHA = "homeassistant.components.zha" DEBUG_COMP_ZIGPY = "zigpy" DEBUG_COMP_ZIGPY_DECONZ = "zigpy_deconz" DEBUG_COMP_ZIGPY_XBEE = "zigpy_xbee" +DEBUG_COMP_ZIGPY_ZIGATE = "zigpy_zigate" DEBUG_LEVEL_CURRENT = "current" DEBUG_LEVEL_ORIGINAL = "original" DEBUG_LEVELS = { @@ -87,6 +88,7 @@ DEBUG_LEVELS = { DEBUG_COMP_ZIGPY: logging.DEBUG, DEBUG_COMP_ZIGPY_XBEE: logging.DEBUG, DEBUG_COMP_ZIGPY_DECONZ: logging.DEBUG, + DEBUG_COMP_ZIGPY_ZIGATE: logging.DEBUG, } DEBUG_RELAY_LOGGERS = [DEBUG_COMP_ZHA, DEBUG_COMP_ZIGPY] @@ -109,6 +111,7 @@ class RadioType(enum.Enum): ezsp = "ezsp" xbee = "xbee" deconz = "deconz" + zigate = "zigate" @classmethod def list(cls): diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 7eff3ebcf3b..7adceb13f54 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -41,6 +41,7 @@ from .const import ( DEBUG_COMP_ZIGPY, DEBUG_COMP_ZIGPY_DECONZ, DEBUG_COMP_ZIGPY_XBEE, + DEBUG_COMP_ZIGPY_ZIGATE, DEBUG_LEVEL_CURRENT, DEBUG_LEVEL_ORIGINAL, DEBUG_LEVELS, @@ -412,6 +413,9 @@ def async_capture_log_levels(): DEBUG_COMP_ZIGPY_DECONZ: logging.getLogger( DEBUG_COMP_ZIGPY_DECONZ ).getEffectiveLevel(), + DEBUG_COMP_ZIGPY_ZIGATE: logging.getLogger( + DEBUG_COMP_ZIGPY_ZIGATE + ).getEffectiveLevel(), } @@ -423,6 +427,7 @@ def async_set_logger_levels(levels): logging.getLogger(DEBUG_COMP_ZIGPY).setLevel(levels[DEBUG_COMP_ZIGPY]) logging.getLogger(DEBUG_COMP_ZIGPY_XBEE).setLevel(levels[DEBUG_COMP_ZIGPY_XBEE]) logging.getLogger(DEBUG_COMP_ZIGPY_DECONZ).setLevel(levels[DEBUG_COMP_ZIGPY_DECONZ]) + logging.getLogger(DEBUG_COMP_ZIGPY_ZIGATE).setLevel(levels[DEBUG_COMP_ZIGPY_ZIGATE]) class LogRelayHandler(logging.Handler): diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 7a1791adc56..3899d601017 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -56,6 +56,11 @@ async def check_zigpy_connection(usb_path, radio_type, database_path): from zigpy_deconz.zigbee.application import ControllerApplication radio = zigpy_deconz.api.Deconz() + elif radio_type == RadioType.zigate.name: + import zigpy_zigate.api + from zigpy_zigate.zigbee.application import ControllerApplication + + radio = zigpy_zigate.api.ZiGate() try: await radio.connect(usb_path, DEFAULT_BAUDRATE) controller = ControllerApplication(radio, database_path) diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 1e1d111fa3e..757a6c3e43f 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -93,17 +93,6 @@ def establish_device_mappings(): ZHA_GW_RADIO_DESCRIPTION: "EZSP", } - def get_xbee_radio(): - import zigpy_xbee.api - from zigpy_xbee.zigbee.application import ControllerApplication - - return {ZHA_GW_RADIO: zigpy_xbee.api.XBee(), CONTROLLER: ControllerApplication} - - RADIO_TYPES[RadioType.xbee.name] = { - ZHA_GW_RADIO: get_xbee_radio, - ZHA_GW_RADIO_DESCRIPTION: "XBee", - } - def get_deconz_radio(): import zigpy_deconz.api from zigpy_deconz.zigbee.application import ControllerApplication @@ -118,6 +107,31 @@ def establish_device_mappings(): ZHA_GW_RADIO_DESCRIPTION: "Deconz", } + def get_xbee_radio(): + import zigpy_xbee.api + from zigpy_xbee.zigbee.application import ControllerApplication + + return {ZHA_GW_RADIO: zigpy_xbee.api.XBee(), CONTROLLER: ControllerApplication} + + RADIO_TYPES[RadioType.xbee.name] = { + ZHA_GW_RADIO: get_xbee_radio, + ZHA_GW_RADIO_DESCRIPTION: "XBee", + } + + def get_zigate_radio(): + import zigpy_zigate.api + from zigpy_zigate.zigbee.application import ControllerApplication + + return { + ZHA_GW_RADIO: zigpy_zigate.api.ZiGate(), + CONTROLLER: ControllerApplication, + } + + RADIO_TYPES[RadioType.zigate.name] = { + ZHA_GW_RADIO: get_zigate_radio, + ZHA_GW_RADIO_DESCRIPTION: "ZiGate", + } + BINARY_SENSOR_CLUSTERS.add(SMARTTHINGS_ACCELERATION_CLUSTER) BINARY_SENSOR_CLUSTERS.add(zcl.clusters.general.OnOff.cluster_id) BINARY_SENSOR_CLUSTERS.add(zcl.clusters.measurement.OccupancySensing.cluster_id) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 88c5f171116..081c77362b9 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -8,7 +8,8 @@ "zha-quirks==0.0.20", "zigpy-deconz==0.2.1", "zigpy-homeassistant==0.7.0", - "zigpy-xbee-homeassistant==0.4.0" + "zigpy-xbee-homeassistant==0.4.0", + "zigpy-zigate==0.1.0" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/requirements_all.txt b/requirements_all.txt index 402bba48314..4019393cf16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1985,5 +1985,8 @@ zigpy-homeassistant==0.7.0 # homeassistant.components.zha zigpy-xbee-homeassistant==0.4.0 +# homeassistant.components.zha +zigpy-zigate==0.1.0 + # homeassistant.components.zoneminder zm-py==0.3.3 From 7ff7c7b9f5b7fcac5483cb975fe88bd747bbe4e8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 6 Aug 2019 07:00:06 +0200 Subject: [PATCH 060/273] UniFi - handle device not having a name (#25713) * Handle device not having a name --- .../components/unifi/device_tracker.py | 20 ++++++++++++++----- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 8ab5140dc48..d0c2684ff53 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -188,7 +188,9 @@ def update_items(controller, async_add_entities, tracked): tracked[client_id] = UniFiClientTracker(client, controller) new_tracked.append(tracked[client_id]) LOGGER.debug( - "New UniFi client tracker %s (%s)", client.hostname, client.mac + "New UniFi client tracker %s (%s)", + client.name or client.hostname, + client.mac, ) if not controller.unifi_config.get(CONF_DONT_TRACK_DEVICES, False): @@ -208,7 +210,11 @@ def update_items(controller, async_add_entities, tracked): tracked[device_id] = UniFiDeviceTracker(device, controller) new_tracked.append(tracked[device_id]) - LOGGER.debug("New UniFi device tracker %s (%s)", device.name, device.mac) + LOGGER.debug( + "New UniFi device tracker %s (%s)", + device.name or device.model, + device.mac, + ) if new_tracked: async_add_entities(new_tracked) @@ -311,7 +317,7 @@ class UniFiDeviceTracker(ScannerEntity): @property def name(self) -> str: """Return the name of the device.""" - return self.device.name + return self.device.name or self.device.model @property def unique_id(self) -> str: @@ -326,14 +332,18 @@ class UniFiDeviceTracker(ScannerEntity): @property def device_info(self): """Return a device description for device registry.""" - return { + info = { "connections": {(CONNECTION_NETWORK_MAC, self.device.mac)}, "manufacturer": ATTR_MANUFACTURER, "model": self.device.model, - "name": self.device.name, "sw_version": self.device.version, } + if self.device.name: + info["name"] = self.device.name + + return info + @property def device_state_attributes(self): """Return the device state attributes.""" diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index e849fd34d25..bcee022e1c4 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/unifi", "requirements": [ - "aiounifi==9" + "aiounifi==10" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 4019393cf16..7ce12cf63f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -170,7 +170,7 @@ aiopvapi==1.6.14 aioswitcher==2019.4.26 # homeassistant.components.unifi -aiounifi==9 +aiounifi==10 # homeassistant.components.wwlln aiowwlln==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 23e6896d890..6cd7c3e398c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -68,7 +68,7 @@ aionotion==1.1.0 aioswitcher==2019.4.26 # homeassistant.components.unifi -aiounifi==9 +aiounifi==10 # homeassistant.components.wwlln aiowwlln==1.0.0 From 9e8df936ac62fe0096f85377f79c2a29239717b1 Mon Sep 17 00:00:00 2001 From: Ross Dargan Date: Tue, 6 Aug 2019 13:39:07 +0100 Subject: [PATCH 061/273] Add ring switch platform (#25612) * Add in a switch platform to ring. * Changes following code review * remove tests for now * remove the request to call update * support the new type of test * update after running black * fix comment * fixes following code review * Remove ring cache file * patch out io code * Move the patches to within a fixture * missing period --- homeassistant/components/ring/switch.py | 108 ++++++ tests/components/ring/common.py | 14 + tests/components/ring/conftest.py | 54 +++ tests/components/ring/test_switch.py | 75 ++++ tests/fixtures/ring_devices.json | 139 ++++++- tests/fixtures/ring_devices_updated.json | 355 ++++++++++++++++++ .../ring_doorbot_siren_on_response.json | 1 + 7 files changed, 745 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/ring/switch.py create mode 100644 tests/components/ring/common.py create mode 100644 tests/components/ring/conftest.py create mode 100644 tests/components/ring/test_switch.py create mode 100644 tests/fixtures/ring_devices_updated.json create mode 100644 tests/fixtures/ring_doorbot_siren_on_response.json diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py new file mode 100644 index 00000000000..3b6bd4ea024 --- /dev/null +++ b/homeassistant/components/ring/switch.py @@ -0,0 +1,108 @@ +"""This component provides HA switch support for Ring Door Bell/Chimes.""" +import logging +from datetime import datetime, timedelta +from homeassistant.components.switch import SwitchDevice +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.core import callback + +from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING + +_LOGGER = logging.getLogger(__name__) + +SIREN_ICON = "mdi:alarm-bell" + + +# It takes a few seconds for the API to correctly return an update indicating +# that the changes have been made. Once we request a change (i.e. a light +# being turned on) we simply wait for this time delta before we allow +# updates to take place. + +SKIP_UPDATES_DELAY = timedelta(seconds=5) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Create the switches for the Ring devices.""" + cameras = hass.data[DATA_RING_STICKUP_CAMS] + switches = [] + for device in cameras: + if device.has_capability("siren"): + switches.append(SirenSwitch(device)) + + add_entities(switches, True) + + +class BaseRingSwitch(SwitchDevice): + """Represents a switch for controlling an aspect of a ring device.""" + + def __init__(self, device, device_type): + """Initialize the switch.""" + self._device = device + self._device_type = device_type + self._unique_id = "{}-{}".format(self._device.id, self._device_type) + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect(self.hass, SIGNAL_UPDATE_RING, self._update_callback) + + @callback + def _update_callback(self): + """Call update method.""" + _LOGGER.debug("Updating Ring sensor %s (callback)", self.name) + self.async_schedule_update_ha_state(True) + + @property + def name(self): + """Name of the device.""" + return "{} {}".format(self._device.name, self._device_type) + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + + @property + def should_poll(self): + """Update controlled via the hub.""" + return False + + +class SirenSwitch(BaseRingSwitch): + """Creates a switch to turn the ring cameras siren on and off.""" + + def __init__(self, device): + """Initialize the switch for a device with a siren.""" + super().__init__(device, "siren") + self._no_updates_until = datetime.now() + self._siren_on = False + + def _set_switch(self, new_state): + """Update switch state, and causes HASS to correctly update.""" + self._device.siren = new_state + self._siren_on = new_state > 0 + self._no_updates_until = datetime.now() + SKIP_UPDATES_DELAY + self.schedule_update_ha_state() + + @property + def is_on(self): + """If the switch is currently on or off.""" + return self._siren_on + + def turn_on(self, **kwargs): + """Turn the siren on for 30 seconds.""" + self._set_switch(1) + + def turn_off(self, **kwargs): + """Turn the siren off.""" + self._set_switch(0) + + @property + def icon(self): + """Return the icon.""" + return SIREN_ICON + + def update(self): + """Update state of the siren.""" + if self._no_updates_until > datetime.now(): + _LOGGER.debug("Skipping update...") + return + self._siren_on = self._device.siren > 0 diff --git a/tests/components/ring/common.py b/tests/components/ring/common.py new file mode 100644 index 00000000000..1228f998618 --- /dev/null +++ b/tests/components/ring/common.py @@ -0,0 +1,14 @@ +"""Common methods used across the tests for ring devices.""" +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_SCAN_INTERVAL +from homeassistant.components.ring import DOMAIN +from homeassistant.setup import async_setup_component + + +async def setup_platform(hass, platform): + """Set up the ring platform and prerequisites.""" + config = { + DOMAIN: {CONF_USERNAME: "foo", CONF_PASSWORD: "bar", CONF_SCAN_INTERVAL: 1000}, + platform: {"platform": DOMAIN}, + } + assert await async_setup_component(hass, platform, config) + await hass.async_block_till_done() diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py new file mode 100644 index 00000000000..14a29f78aae --- /dev/null +++ b/tests/components/ring/conftest.py @@ -0,0 +1,54 @@ +"""Configuration for Ring tests.""" +import requests_mock +import pytest +from tests.common import load_fixture +from asynctest import patch + + +@pytest.fixture(name="ring_mock") +def ring_save_mock(): + """Fixture to mock a ring.""" + with patch("ring_doorbell._exists_cache", return_value=False): + with patch("ring_doorbell._save_cache", return_value=True) as save_mock: + yield save_mock + + +@pytest.fixture(name="requests_mock") +def requests_mock_fixture(ring_mock): + """Fixture to provide a requests mocker.""" + with requests_mock.mock() as mock: + # Note all devices have an id of 987652, but a different device_id. + # the device_id is used as our unique_id, but the id is what is sent + # to the APIs, which is why every mock uses that id. + + # Mocks the response for authenticating + mock.post( + "https://oauth.ring.com/oauth/token", text=load_fixture("ring_oauth.json") + ) + # Mocks the response for getting the login session + mock.post( + "https://api.ring.com/clients_api/session", + text=load_fixture("ring_session.json"), + ) + # Mocks the response for getting all the devices + mock.get( + "https://api.ring.com/clients_api/ring_devices", + text=load_fixture("ring_devices.json"), + ) + # Mocks the response for getting the history of a device + mock.get( + "https://api.ring.com/clients_api/doorbots/987652/history", + text=load_fixture("ring_doorbots.json"), + ) + # Mocks the response for getting the health of a device + mock.get( + "https://api.ring.com/clients_api/doorbots/987652/health", + text=load_fixture("ring_doorboot_health_attrs.json"), + ) + # Mocks the response for getting a chimes health + mock.get( + "https://api.ring.com/clients_api/chimes/999999/health", + text=load_fixture("ring_chime_health_attrs.json"), + ) + + yield mock diff --git a/tests/components/ring/test_switch.py b/tests/components/ring/test_switch.py new file mode 100644 index 00000000000..59f4e9061f4 --- /dev/null +++ b/tests/components/ring/test_switch.py @@ -0,0 +1,75 @@ +"""The tests for the Ring switch platform.""" +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from tests.common import load_fixture +from .common import setup_platform + + +async def test_entity_registry(hass, requests_mock): + """Tests that the devices are registed in the entity registry.""" + await setup_platform(hass, SWITCH_DOMAIN) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entry = entity_registry.async_get("switch.front_siren") + assert entry.unique_id == "aacdef123-siren" + + entry = entity_registry.async_get("switch.internal_siren") + assert entry.unique_id == "aacdef124-siren" + + +async def test_siren_off_reports_correctly(hass, requests_mock): + """Tests that the initial state of a device that should be off is correct.""" + await setup_platform(hass, SWITCH_DOMAIN) + + state = hass.states.get("switch.front_siren") + assert state.state == "off" + assert state.attributes.get("friendly_name") == "Front siren" + + +async def test_siren_on_reports_correctly(hass, requests_mock): + """Tests that the initial state of a device that should be on is correct.""" + await setup_platform(hass, SWITCH_DOMAIN) + + state = hass.states.get("switch.internal_siren") + assert state.state == "on" + assert state.attributes.get("friendly_name") == "Internal siren" + assert state.attributes.get("icon") == "mdi:alarm-bell" + + +async def test_siren_can_be_turned_on(hass, requests_mock): + """Tests the siren turns on correctly.""" + await setup_platform(hass, SWITCH_DOMAIN) + + # Mocks the response for turning a siren on + requests_mock.put( + "https://api.ring.com/clients_api/doorbots/987652/siren_on", + text=load_fixture("ring_doorbot_siren_on_response.json"), + ) + + state = hass.states.get("switch.front_siren") + assert state.state == "off" + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": "switch.front_siren"}, blocking=True + ) + + state = hass.states.get("switch.front_siren") + assert state.state == "on" + + +async def test_updates_work(hass, requests_mock): + """Tests the update service works correctly.""" + await setup_platform(hass, SWITCH_DOMAIN) + state = hass.states.get("switch.front_siren") + assert state.state == "off" + # Changes the return to indicate that the siren is now on. + requests_mock.get( + "https://api.ring.com/clients_api/ring_devices", + text=load_fixture("ring_devices_updated.json"), + ) + + await hass.services.async_call("ring", "update", {}, blocking=True) + + await hass.async_block_till_done() + + state = hass.states.get("switch.front_siren") + assert state.state == "on" diff --git a/tests/fixtures/ring_devices.json b/tests/fixtures/ring_devices.json index 3e9171789d9..f5dd1c1f091 100644 --- a/tests/fixtures/ring_devices.json +++ b/tests/fixtures/ring_devices.json @@ -214,5 +214,142 @@ "subscribed": true, "subscribed_motions": true, "time_zone": "America/New_York" - }] + }, + { + "address": "123 Main St", + "alerts": {"connection": "online"}, + "battery_life": 80, + "description": "Internal", + "device_id": "aacdef124", + "external_connection": false, + "features": { + "advanced_motion_enabled": false, + "motion_message_enabled": false, + "motions_enabled": true, + "night_vision_enabled": false, + "people_only_enabled": false, + "shadow_correction_enabled": false, + "show_recordings": true}, + "firmware_version": "1.9.3", + "id": 987652, + "kind": "hp_cam_v1", + "latitude": 12.000000, + "led_status": "off", + "location_id": null, + "longitude": -70.12345, + "motion_snooze": {"scheduled": true}, + "night_mode_status": "false", + "owned": true, + "owner": { + "email": "foo@bar.org", + "first_name": "Foo", + "id": 999999, + "last_name": "Bar"}, + "ring_cam_light_installed": "false", + "ring_id": null, + "settings": { + "chime_settings": { + "duration": 10, + "enable": true, + "type": 0}, + "doorbell_volume": 11, + "enable_vod": true, + "floodlight_settings": { + "duration": 30, + "priority": 0}, + "light_schedule_settings": { + "end_hour": 0, + "end_minute": 0, + "start_hour": 0, + "start_minute": 0}, + "live_view_preset_profile": "highest", + "live_view_presets": [ + "low", + "middle", + "high", + "highest"], + "motion_announcement": false, + "motion_snooze_preset_profile": "low", + "motion_snooze_presets": [ + "none", + "low", + "medium", + "high"], + "motion_zones": { + "active_motion_filter": 1, + "advanced_object_settings": { + "human_detection_confidence": { + "day": 0.7, + "night": 0.7}, + "motion_zone_overlap": { + "day": 0.1, + "night": 0.2}, + "object_size_maximum": { + "day": 0.8, + "night": 0.8}, + "object_size_minimum": { + "day": 0.03, + "night": 0.05}, + "object_time_overlap": { + "day": 0.1, + "night": 0.6} + }, + "enable_audio": false, + "pir_settings": { + "sensitivity1": 1, + "sensitivity2": 1, + "sensitivity3": 1, + "zone_mask": 6}, + "sensitivity": 5, + "zone1": { + "name": "Zone 1", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}, + "zone2": { + "name": "Zone 2", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}, + "zone3": { + "name": "Zone 3", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}}, + "pir_motion_zones": [0, 1, 1], + "pir_settings": { + "sensitivity1": 1, + "sensitivity2": 1, + "sensitivity3": 1, + "zone_mask": 6}, + "stream_setting": 0, + "video_settings": { + "ae_level": 0, + "birton": null, + "brightness": 0, + "contrast": 64, + "saturation": 80}}, + "siren_status": {"seconds_remaining": 30}, + "stolen": false, + "subscribed": true, + "subscribed_motions": true, + "time_zone": "America/New_York"}] } diff --git a/tests/fixtures/ring_devices_updated.json b/tests/fixtures/ring_devices_updated.json new file mode 100644 index 00000000000..bc621f81be8 --- /dev/null +++ b/tests/fixtures/ring_devices_updated.json @@ -0,0 +1,355 @@ +{ + "authorized_doorbots": [], + "chimes": [ + { + "address": "123 Main St", + "alerts": {"connection": "online"}, + "description": "Downstairs", + "device_id": "abcdef123", + "do_not_disturb": {"seconds_left": 0}, + "features": {"ringtones_enabled": true}, + "firmware_version": "1.2.3", + "id": 999999, + "kind": "chime", + "latitude": 12.000000, + "longitude": -70.12345, + "owned": true, + "owner": { + "email": "foo@bar.org", + "first_name": "Marcelo", + "id": 999999, + "last_name": "Assistant"}, + "settings": { + "ding_audio_id": null, + "ding_audio_user_id": null, + "motion_audio_id": null, + "motion_audio_user_id": null, + "volume": 2}, + "time_zone": "America/New_York"}], + "doorbots": [ + { + "address": "123 Main St", + "alerts": {"connection": "online"}, + "battery_life": 4081, + "description": "Front Door", + "device_id": "aacdef123", + "external_connection": false, + "features": { + "advanced_motion_enabled": false, + "motion_message_enabled": false, + "motions_enabled": true, + "people_only_enabled": false, + "shadow_correction_enabled": false, + "show_recordings": true}, + "firmware_version": "1.4.26", + "id": 987652, + "kind": "lpd_v1", + "latitude": 12.000000, + "longitude": -70.12345, + "motion_snooze": null, + "owned": true, + "owner": { + "email": "foo@bar.org", + "first_name": "Home", + "id": 999999, + "last_name": "Assistant"}, + "settings": { + "chime_settings": { + "duration": 3, + "enable": true, + "type": 0}, + "doorbell_volume": 1, + "enable_vod": true, + "live_view_preset_profile": "highest", + "live_view_presets": [ + "low", + "middle", + "high", + "highest"], + "motion_announcement": false, + "motion_snooze_preset_profile": "low", + "motion_snooze_presets": [ + "null", + "low", + "medium", + "high"]}, + "subscribed": true, + "subscribed_motions": true, + "time_zone": "America/New_York"}], + "stickup_cams": [ + { + "address": "123 Main St", + "alerts": {"connection": "online"}, + "battery_life": 80, + "description": "Front", + "device_id": "aacdef123", + "external_connection": false, + "features": { + "advanced_motion_enabled": false, + "motion_message_enabled": false, + "motions_enabled": true, + "night_vision_enabled": false, + "people_only_enabled": false, + "shadow_correction_enabled": false, + "show_recordings": true}, + "firmware_version": "1.9.3", + "id": 987652, + "kind": "hp_cam_v1", + "latitude": 12.000000, + "led_status": "off", + "location_id": null, + "longitude": -70.12345, + "motion_snooze": {"scheduled": true}, + "night_mode_status": "false", + "owned": true, + "owner": { + "email": "foo@bar.org", + "first_name": "Foo", + "id": 999999, + "last_name": "Bar"}, + "ring_cam_light_installed": "false", + "ring_id": null, + "settings": { + "chime_settings": { + "duration": 10, + "enable": true, + "type": 0}, + "doorbell_volume": 11, + "enable_vod": true, + "floodlight_settings": { + "duration": 30, + "priority": 0}, + "light_schedule_settings": { + "end_hour": 0, + "end_minute": 0, + "start_hour": 0, + "start_minute": 0}, + "live_view_preset_profile": "highest", + "live_view_presets": [ + "low", + "middle", + "high", + "highest"], + "motion_announcement": false, + "motion_snooze_preset_profile": "low", + "motion_snooze_presets": [ + "none", + "low", + "medium", + "high"], + "motion_zones": { + "active_motion_filter": 1, + "advanced_object_settings": { + "human_detection_confidence": { + "day": 0.7, + "night": 0.7}, + "motion_zone_overlap": { + "day": 0.1, + "night": 0.2}, + "object_size_maximum": { + "day": 0.8, + "night": 0.8}, + "object_size_minimum": { + "day": 0.03, + "night": 0.05}, + "object_time_overlap": { + "day": 0.1, + "night": 0.6} + }, + "enable_audio": false, + "pir_settings": { + "sensitivity1": 1, + "sensitivity2": 1, + "sensitivity3": 1, + "zone_mask": 6}, + "sensitivity": 5, + "zone1": { + "name": "Zone 1", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}, + "zone2": { + "name": "Zone 2", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}, + "zone3": { + "name": "Zone 3", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}}, + "pir_motion_zones": [0, 1, 1], + "pir_settings": { + "sensitivity1": 1, + "sensitivity2": 1, + "sensitivity3": 1, + "zone_mask": 6}, + "stream_setting": 0, + "video_settings": { + "ae_level": 0, + "birton": null, + "brightness": 0, + "contrast": 64, + "saturation": 80}}, + "siren_status": {"seconds_remaining": 30}, + "stolen": false, + "subscribed": true, + "subscribed_motions": true, + "time_zone": "America/New_York" + }, + { + "address": "123 Main St", + "alerts": {"connection": "online"}, + "battery_life": 80, + "description": "Internal", + "device_id": "aacdef124", + "external_connection": false, + "features": { + "advanced_motion_enabled": false, + "motion_message_enabled": false, + "motions_enabled": true, + "night_vision_enabled": false, + "people_only_enabled": false, + "shadow_correction_enabled": false, + "show_recordings": true}, + "firmware_version": "1.9.3", + "id": 987652, + "kind": "hp_cam_v1", + "latitude": 12.000000, + "led_status": "off", + "location_id": null, + "longitude": -70.12345, + "motion_snooze": {"scheduled": true}, + "night_mode_status": "false", + "owned": true, + "owner": { + "email": "foo@bar.org", + "first_name": "Foo", + "id": 999999, + "last_name": "Bar"}, + "ring_cam_light_installed": "false", + "ring_id": null, + "settings": { + "chime_settings": { + "duration": 10, + "enable": true, + "type": 0}, + "doorbell_volume": 11, + "enable_vod": true, + "floodlight_settings": { + "duration": 30, + "priority": 0}, + "light_schedule_settings": { + "end_hour": 0, + "end_minute": 0, + "start_hour": 0, + "start_minute": 0}, + "live_view_preset_profile": "highest", + "live_view_presets": [ + "low", + "middle", + "high", + "highest"], + "motion_announcement": false, + "motion_snooze_preset_profile": "low", + "motion_snooze_presets": [ + "none", + "low", + "medium", + "high"], + "motion_zones": { + "active_motion_filter": 1, + "advanced_object_settings": { + "human_detection_confidence": { + "day": 0.7, + "night": 0.7}, + "motion_zone_overlap": { + "day": 0.1, + "night": 0.2}, + "object_size_maximum": { + "day": 0.8, + "night": 0.8}, + "object_size_minimum": { + "day": 0.03, + "night": 0.05}, + "object_time_overlap": { + "day": 0.1, + "night": 0.6} + }, + "enable_audio": false, + "pir_settings": { + "sensitivity1": 1, + "sensitivity2": 1, + "sensitivity3": 1, + "zone_mask": 6}, + "sensitivity": 5, + "zone1": { + "name": "Zone 1", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}, + "zone2": { + "name": "Zone 2", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}, + "zone3": { + "name": "Zone 3", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}}, + "pir_motion_zones": [0, 1, 1], + "pir_settings": { + "sensitivity1": 1, + "sensitivity2": 1, + "sensitivity3": 1, + "zone_mask": 6}, + "stream_setting": 0, + "video_settings": { + "ae_level": 0, + "birton": null, + "brightness": 0, + "contrast": 64, + "saturation": 80}}, + "siren_status": {"seconds_remaining": 30}, + "stolen": false, + "subscribed": true, + "subscribed_motions": true, + "time_zone": "America/New_York"}] +} diff --git a/tests/fixtures/ring_doorbot_siren_on_response.json b/tests/fixtures/ring_doorbot_siren_on_response.json new file mode 100644 index 00000000000..6bf91f88299 --- /dev/null +++ b/tests/fixtures/ring_doorbot_siren_on_response.json @@ -0,0 +1 @@ +{"started_at":"2019-07-28T16:58:27.593+00:00","duration":30,"ends_at":"2019-07-28T16:58:57.593+00:00","seconds_remaining":30} \ No newline at end of file From 778cd7da32300b9c4226a44b46988099ec17b74b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 5 Aug 2019 22:31:57 -0700 Subject: [PATCH 062/273] Updated frontend to 20190805.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 4cea624b4cd..6d25e846db9 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==20190804.0" + "home-assistant-frontend==20190805.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 89465568c65..a1148063aee 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.16 -home-assistant-frontend==20190804.0 +home-assistant-frontend==20190805.0 importlib-metadata==0.18 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7ce12cf63f8..95af9b4c9e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -627,7 +627,7 @@ hole==0.3.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190804.0 +home-assistant-frontend==20190805.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6cd7c3e398c..c4982650a3e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -174,7 +174,7 @@ hdate==0.8.8 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190804.0 +home-assistant-frontend==20190805.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From a76bb5e82c8d1856ac8bc0ddc5815c838ef84a5c Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 6 Aug 2019 17:03:08 +0100 Subject: [PATCH 063/273] initial commit (#25731) --- homeassistant/components/incomfort/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index 13c77cd33ff..8b5f461c8af 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.3.0" + "incomfort-client==0.3.1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 95af9b4c9e6..418ebbdd743 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -669,7 +669,7 @@ iglo==1.2.7 ihcsdk==2.3.0 # homeassistant.components.incomfort -incomfort-client==0.3.0 +incomfort-client==0.3.1 # homeassistant.components.influxdb influxdb==5.2.0 From 54d6acf87ac6fe9199936a58856f54571006bfe0 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Tue, 6 Aug 2019 19:03:52 +0300 Subject: [PATCH 064/273] Bump hdate==0.9.0 (use pytz instead of dateutil) (#25726) Use new hdate version of library which uses pytz for timezones. dateutil expects /usr/share/timezone files, as these are not available in the docker image and in HASSIO, the timezone offsets are broken. This should fix - #23032 - #18731 --- homeassistant/components/jewish_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index 5c3eee48ead..fdc1d2943e6 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -3,7 +3,7 @@ "name": "Jewish calendar", "documentation": "https://www.home-assistant.io/components/jewish_calendar", "requirements": [ - "hdate==0.8.8" + "hdate==0.9.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 418ebbdd743..21bc1127c5a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -603,7 +603,7 @@ hass-nabucasa==0.16 hbmqtt==0.9.4 # homeassistant.components.jewish_calendar -hdate==0.8.8 +hdate==0.9.0 # homeassistant.components.heatmiser heatmiserV3==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c4982650a3e..a0621a28221 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -168,7 +168,7 @@ hass-nabucasa==0.16 hbmqtt==0.9.4 # homeassistant.components.jewish_calendar -hdate==0.8.8 +hdate==0.9.0 # homeassistant.components.workday holidays==0.9.11 From e24d3f15fdddeaa18ab537d42a7edf50db4e1f9b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 6 Aug 2019 11:46:07 -0700 Subject: [PATCH 065/273] Revert emulated hue changes (#25732) --- .../components/emulated_hue/hue_api.py | 26 ++++++------------- tests/components/emulated_hue/test_hue_api.py | 14 ---------- 2 files changed, 8 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 6d59d777e8b..1b08b43c9af 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -562,25 +562,15 @@ def get_entity_state(config, entity): def entity_to_json(config, entity, state): """Convert an entity to its Hue bridge JSON representation.""" - entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if entity_features & SUPPORT_BRIGHTNESS: - return { - "state": { - HUE_API_STATE_ON: state[STATE_ON], - HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], - HUE_API_STATE_HUE: state[STATE_HUE], - HUE_API_STATE_SAT: state[STATE_SATURATION], - "reachable": True, - }, - "type": "Dimmable light", - "name": config.get_entity_name(entity), - "modelid": "HASS123", - "uniqueid": entity.entity_id, - "swversion": "123", - } return { - "state": {HUE_API_STATE_ON: state[STATE_ON], "reachable": True}, - "type": "On/off light", + "state": { + HUE_API_STATE_ON: state[STATE_ON], + HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], + HUE_API_STATE_HUE: state[STATE_HUE], + HUE_API_STATE_SAT: state[STATE_SATURATION], + "reachable": True, + }, + "type": "Dimmable light", "name": config.get_entity_name(entity), "modelid": "HASS123", "uniqueid": entity.entity_id, diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 02f24f5afba..57f29a4ef61 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -128,9 +128,6 @@ def hass_hue(loop, hass): kitchen_light_entity.entity_id, kitchen_light_entity.state, attributes=attrs ) - # create a lamp without brightness support - hass.states.async_set("light.no_brightness", "on", {}) - # Ceiling Fan is explicitly excluded from being exposed ceiling_fan_entity = hass.states.get("fan.ceiling_fan") attrs = dict(ceiling_fan_entity.attributes) @@ -221,17 +218,6 @@ def test_discover_lights(hue_client): assert "climate.ecobee" not in devices -@asyncio.coroutine -def test_light_without_brightness_supported(hass_hue, hue_client): - """Test that light without brightness is supported.""" - light_without_brightness_json = yield from perform_get_light_state( - hue_client, "light.no_brightness", 200 - ) - - assert light_without_brightness_json["state"][HUE_API_STATE_ON] is True - assert light_without_brightness_json["type"] == "On/off light" - - @asyncio.coroutine def test_get_light_state(hass_hue, hue_client): """Test the getting of light state.""" From c76531a36618bf097c6450f23a5635cb37a53f46 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 6 Aug 2019 23:55:36 +0200 Subject: [PATCH 066/273] Fix last seen not available on certain devices (#25735) --- homeassistant/components/unifi/device_tracker.py | 12 +++++++----- tests/components/unifi/test_device_tracker.py | 13 ++++++++++++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index d0c2684ff53..89d3fce515e 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -301,11 +301,10 @@ class UniFiDeviceTracker(ScannerEntity): CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME ) - if ( - self.device.last_seen - and dt_util.utcnow() - - dt_util.utc_from_timestamp(float(self.device.last_seen)) - ) < detection_time: + if self.device.last_seen and ( + dt_util.utcnow() - dt_util.utc_from_timestamp(float(self.device.last_seen)) + < detection_time + ): return True return False @@ -347,6 +346,9 @@ class UniFiDeviceTracker(ScannerEntity): @property def device_state_attributes(self): """Return the device state attributes.""" + if not self.device.last_seen: + return {} + attributes = {} attributes["upgradable"] = self.device.upgradable diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 9fca9d21a5b..0d8d631d8ff 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -73,6 +73,17 @@ DEVICE_1 = { "upgradable": False, "version": "4.0.42.10433", } +DEVICE_2 = { + "board_rev": 3, + "device_id": "mock-id", + "has_fan": True, + "ip": "10.0.1.1", + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "device_1", + "type": "usw", + "version": "4.0.42.10433", +} CONTROLLER_DATA = { CONF_HOST: "mock-host", @@ -167,7 +178,7 @@ async def test_no_clients(hass, mock_controller): async def test_tracked_devices(hass, mock_controller): """Test the update_items function with some clients.""" mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2, CLIENT_3]) - mock_controller.mock_device_responses.append([DEVICE_1]) + mock_controller.mock_device_responses.append([DEVICE_1, DEVICE_2]) mock_controller.unifi_config = {unifi_dt.CONF_SSID_FILTER: ["ssid"]} await setup_controller(hass, mock_controller) From 98eb8efc6b808420c5da13db89ccabd65e93e1af Mon Sep 17 00:00:00 2001 From: Ross Dargan Date: Tue, 6 Aug 2019 22:55:54 +0100 Subject: [PATCH 067/273] Add ring light platform (#25733) * Add support for turning a flood light on and off * changes following black * update following code review, and test fix * fix naming --- homeassistant/components/ring/light.py | 97 ++++++++++++++++++++++++ tests/components/ring/test_light.py | 75 ++++++++++++++++++ tests/components/ring/test_switch.py | 1 + tests/fixtures/ring_devices.json | 2 +- tests/fixtures/ring_devices_updated.json | 2 +- 5 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/ring/light.py create mode 100644 tests/components/ring/test_light.py diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py new file mode 100644 index 00000000000..bd7ea3a3679 --- /dev/null +++ b/homeassistant/components/ring/light.py @@ -0,0 +1,97 @@ +"""This component provides HA switch support for Ring Door Bell/Chimes.""" +import logging +from datetime import datetime, timedelta +from homeassistant.components.light import Light +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.core import callback + +from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING + +_LOGGER = logging.getLogger(__name__) + + +# It takes a few seconds for the API to correctly return an update indicating +# that the changes have been made. Once we request a change (i.e. a light +# being turned on) we simply wait for this time delta before we allow +# updates to take place. + +SKIP_UPDATES_DELAY = timedelta(seconds=5) + +ON_STATE = "on" +OFF_STATE = "off" + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Create the lights for the Ring devices.""" + cameras = hass.data[DATA_RING_STICKUP_CAMS] + lights = [] + + for device in cameras: + if device.has_capability("light"): + lights.append(RingLight(device)) + + add_entities(lights, True) + + +class RingLight(Light): + """Creates a switch to turn the ring cameras light on and off.""" + + def __init__(self, device): + """Initialize the light.""" + self._device = device + self._unique_id = self._device.id + self._light_on = False + self._no_updates_until = datetime.now() + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect(self.hass, SIGNAL_UPDATE_RING, self._update_callback) + + @callback + def _update_callback(self): + """Call update method.""" + _LOGGER.debug("Updating Ring light %s (callback)", self.name) + self.async_schedule_update_ha_state(True) + + @property + def name(self): + """Name of the light.""" + return "{} light".format(self._device.name) + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + + @property + def should_poll(self): + """Update controlled via the hub.""" + return False + + @property + def is_on(self): + """If the switch is currently on or off.""" + return self._light_on + + def _set_light(self, new_state): + """Update light state, and causes HASS to correctly update.""" + self._device.lights = new_state + self._light_on = new_state == ON_STATE + self._no_updates_until = datetime.now() + SKIP_UPDATES_DELAY + self.async_schedule_update_ha_state(True) + + def turn_on(self, **kwargs): + """Turn the light on for 30 seconds.""" + self._set_light(ON_STATE) + + def turn_off(self, **kwargs): + """Turn the light off.""" + self._set_light(OFF_STATE) + + def update(self): + """Update current state of the light.""" + if self._no_updates_until > datetime.now(): + _LOGGER.debug("Skipping update...") + return + + self._light_on = self._device.lights == ON_STATE diff --git a/tests/components/ring/test_light.py b/tests/components/ring/test_light.py new file mode 100644 index 00000000000..e07867c19b2 --- /dev/null +++ b/tests/components/ring/test_light.py @@ -0,0 +1,75 @@ +"""The tests for the Ring light platform.""" +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from tests.common import load_fixture +from .common import setup_platform + + +async def test_entity_registry(hass, requests_mock): + """Tests that the devices are registed in the entity registry.""" + await setup_platform(hass, LIGHT_DOMAIN) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entry = entity_registry.async_get("light.front_light") + assert entry.unique_id == "aacdef123" + + entry = entity_registry.async_get("light.internal_light") + assert entry.unique_id == "aacdef124" + + +async def test_light_off_reports_correctly(hass, requests_mock): + """Tests that the initial state of a device that should be off is correct.""" + await setup_platform(hass, LIGHT_DOMAIN) + + state = hass.states.get("light.front_light") + assert state.state == "off" + assert state.attributes.get("friendly_name") == "Front light" + + +async def test_light_on_reports_correctly(hass, requests_mock): + """Tests that the initial state of a device that should be on is correct.""" + await setup_platform(hass, LIGHT_DOMAIN) + + state = hass.states.get("light.internal_light") + assert state.state == "on" + assert state.attributes.get("friendly_name") == "Internal light" + + +async def test_light_can_be_turned_on(hass, requests_mock): + """Tests the light turns on correctly.""" + await setup_platform(hass, LIGHT_DOMAIN) + + # Mocks the response for turning a light on + requests_mock.put( + "https://api.ring.com/clients_api/doorbots/987652/floodlight_light_on", + text=load_fixture("ring_doorbot_siren_on_response.json"), + ) + + state = hass.states.get("light.front_light") + assert state.state == "off" + + await hass.services.async_call( + "light", "turn_on", {"entity_id": "light.front_light"}, blocking=True + ) + await hass.async_block_till_done() + + state = hass.states.get("light.front_light") + assert state.state == "on" + + +async def test_updates_work(hass, requests_mock): + """Tests the update service works correctly.""" + await setup_platform(hass, LIGHT_DOMAIN) + state = hass.states.get("light.front_light") + assert state.state == "off" + # Changes the return to indicate that the light is now on. + requests_mock.get( + "https://api.ring.com/clients_api/ring_devices", + text=load_fixture("ring_devices_updated.json"), + ) + + await hass.services.async_call("ring", "update", {}, blocking=True) + + await hass.async_block_till_done() + + state = hass.states.get("light.front_light") + assert state.state == "on" diff --git a/tests/components/ring/test_switch.py b/tests/components/ring/test_switch.py index 59f4e9061f4..864d16466da 100644 --- a/tests/components/ring/test_switch.py +++ b/tests/components/ring/test_switch.py @@ -52,6 +52,7 @@ async def test_siren_can_be_turned_on(hass, requests_mock): "switch", "turn_on", {"entity_id": "switch.front_siren"}, blocking=True ) + await hass.async_block_till_done() state = hass.states.get("switch.front_siren") assert state.state == "on" diff --git a/tests/fixtures/ring_devices.json b/tests/fixtures/ring_devices.json index f5dd1c1f091..557aef3535c 100644 --- a/tests/fixtures/ring_devices.json +++ b/tests/fixtures/ring_devices.json @@ -234,7 +234,7 @@ "id": 987652, "kind": "hp_cam_v1", "latitude": 12.000000, - "led_status": "off", + "led_status": "on", "location_id": null, "longitude": -70.12345, "motion_snooze": {"scheduled": true}, diff --git a/tests/fixtures/ring_devices_updated.json b/tests/fixtures/ring_devices_updated.json index bc621f81be8..fa3c0586101 100644 --- a/tests/fixtures/ring_devices_updated.json +++ b/tests/fixtures/ring_devices_updated.json @@ -96,7 +96,7 @@ "id": 987652, "kind": "hp_cam_v1", "latitude": 12.000000, - "led_status": "off", + "led_status": "on", "location_id": null, "longitude": -70.12345, "motion_snooze": {"scheduled": true}, From 152a9eb4668ff27132020994bd1487e6e12d84e4 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 6 Aug 2019 20:01:14 -0400 Subject: [PATCH 068/273] Use more decorators for ZHA Core registries. (#25737) * Move ZIGBEE_CHANNEL_REGISTRY to ZHA core registries. * Refactor more ZHA Core registries to use decorator. * Cleanup. * Use relative imports for component. * Flake8. * Make pylint happy again. --- .../components/zha/core/channels/__init__.py | 3 - .../components/zha/core/channels/closures.py | 9 ++- .../components/zha/core/channels/general.py | 77 ++++++++++--------- .../zha/core/channels/homeautomation.py | 26 +++++-- .../components/zha/core/channels/hvac.py | 14 ++-- .../components/zha/core/channels/lighting.py | 10 ++- .../components/zha/core/channels/lightlink.py | 6 +- .../zha/core/channels/measurement.py | 24 ++++-- .../components/zha/core/channels/protocol.py | 51 +++++++----- .../components/zha/core/channels/security.py | 10 ++- .../zha/core/channels/smartenergy.py | 27 +++---- .../components/zha/core/decorators.py | 37 +++++++++ .../components/zha/core/discovery.py | 8 +- homeassistant/components/zha/core/helpers.py | 3 +- .../components/zha/core/registries.py | 60 ++++----------- tests/components/zha/test_channels.py | 8 +- 16 files changed, 212 insertions(+), 161 deletions(-) create mode 100644 homeassistant/components/zha/core/decorators.py diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 246c17d83d6..20756f26b72 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -13,7 +13,6 @@ from random import uniform from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.util.decorator import Registry from ..const import ( CHANNEL_ATTRIBUTE, @@ -30,8 +29,6 @@ from ..registries import CLUSTER_REPORT_CONFIGS _LOGGER = logging.getLogger(__name__) -ZIGBEE_CHANNEL_REGISTRY = Registry() - def parse_and_log_command(channel, tsn, command_id, args): """Parse and log a zigbee cluster command.""" diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index 4bf3bb0fe00..0559c4a1f76 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -11,13 +11,14 @@ import zigpy.zcl.clusters.closures as closures from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from . import ZIGBEE_CHANNEL_REGISTRY, ZigbeeChannel +from . import ZigbeeChannel +from .. import registries from ..const import REPORT_CONFIG_IMMEDIATE, SIGNAL_ATTR_UPDATED _LOGGER = logging.getLogger(__name__) -@ZIGBEE_CHANNEL_REGISTRY.register(closures.DoorLock.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(closures.DoorLock.cluster_id) class DoorLockChannel(ZigbeeChannel): """Door lock channel.""" @@ -54,14 +55,14 @@ class DoorLockChannel(ZigbeeChannel): await super().async_initialize(from_cache) -@ZIGBEE_CHANNEL_REGISTRY.register(closures.Shade.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(closures.Shade.cluster_id) class Shade(ZigbeeChannel): """Shade channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(closures.WindowCovering.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(closures.WindowCovering.cluster_id) class WindowCovering(ZigbeeChannel): """Window channel.""" diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index c5706c99afd..6a828ef1ad8 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -12,12 +12,8 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later -from . import ( - ZIGBEE_CHANNEL_REGISTRY, - AttributeListeningChannel, - ZigbeeChannel, - parse_and_log_command, -) +from . import AttributeListeningChannel, ZigbeeChannel, parse_and_log_command +from .. import registries from ..const import ( REPORT_CONFIG_ASAP, REPORT_CONFIG_BATTERY_SAVE, @@ -32,42 +28,43 @@ from ..helpers import get_attr_id_by_name _LOGGER = logging.getLogger(__name__) -@ZIGBEE_CHANNEL_REGISTRY.register(general.Alarms.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Alarms.cluster_id) class Alarms(ZigbeeChannel): """Alarms channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogInput.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogInput.cluster_id) class AnalogInput(AttributeListeningChannel): """Analog Input channel.""" REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] -@ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogOutput.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogOutput.cluster_id) class AnalogOutput(AttributeListeningChannel): """Analog Output channel.""" REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] -@ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogValue.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogValue.cluster_id) class AnalogValue(AttributeListeningChannel): """Analog Value channel.""" REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] -@ZIGBEE_CHANNEL_REGISTRY.register(general.ApplianceControl.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.ApplianceControl.cluster_id) class ApplianceContorl(ZigbeeChannel): """Appliance Control channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(general.Basic.cluster_id) +@registries.CHANNEL_ONLY_CLUSTERS.register(general.Basic.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Basic.cluster_id) class BasicChannel(ZigbeeChannel): """Channel to interact with the basic cluster.""" @@ -106,63 +103,66 @@ class BasicChannel(ZigbeeChannel): return self._power_source -@ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryInput.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryInput.cluster_id) class BinaryInput(AttributeListeningChannel): """Binary Input channel.""" REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] -@ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryOutput.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryOutput.cluster_id) class BinaryOutput(AttributeListeningChannel): """Binary Output channel.""" REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] -@ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryValue.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryValue.cluster_id) class BinaryValue(AttributeListeningChannel): """Binary Value channel.""" REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] -@ZIGBEE_CHANNEL_REGISTRY.register(general.Commissioning.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Commissioning.cluster_id) class Commissioning(ZigbeeChannel): """Commissioning channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(general.DeviceTemperature.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.DeviceTemperature.cluster_id) class DeviceTemperature(ZigbeeChannel): - """Device Temperatur channel.""" + """Device Temperature channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(general.GreenPowerProxy.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.GreenPowerProxy.cluster_id) class GreenPowerProxy(ZigbeeChannel): """Green Power Proxy channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(general.Groups.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Groups.cluster_id) class Groups(ZigbeeChannel): """Groups channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(general.Identify.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Identify.cluster_id) class Identify(ZigbeeChannel): """Identify channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(general.LevelControl.cluster_id) +@registries.BINDABLE_CLUSTERS.register(general.LevelControl.cluster_id) +@registries.EVENT_RELAY_CLUSTERS.register(general.LevelControl.cluster_id) +@registries.LIGHT_CLUSTERS.register(general.LevelControl.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.LevelControl.cluster_id) class LevelControlChannel(ZigbeeChannel): """Channel for the LevelControl Zigbee cluster.""" @@ -207,28 +207,33 @@ class LevelControlChannel(ZigbeeChannel): await super().async_initialize(from_cache) -@ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateInput.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateInput.cluster_id) class MultistateInput(AttributeListeningChannel): """Multistate Input channel.""" REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] -@ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateOutput.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateOutput.cluster_id) class MultistateOutput(AttributeListeningChannel): """Multistate Output channel.""" REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] -@ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateValue.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateValue.cluster_id) class MultistateValue(AttributeListeningChannel): """Multistate Value channel.""" REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] -@ZIGBEE_CHANNEL_REGISTRY.register(general.OnOff.cluster_id) +@registries.BINARY_SENSOR_CLUSTERS.register(general.OnOff.cluster_id) +@registries.BINDABLE_CLUSTERS.register(general.OnOff.cluster_id) +@registries.EVENT_RELAY_CLUSTERS.register(general.OnOff.cluster_id) +@registries.LIGHT_CLUSTERS.register(general.OnOff.cluster_id) +@registries.SWITCH_CLUSTERS.register(general.OnOff.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.OnOff.cluster_id) class OnOffChannel(ZigbeeChannel): """Channel for the OnOff Zigbee cluster.""" @@ -302,35 +307,36 @@ class OnOffChannel(ZigbeeChannel): await super().async_update() -@ZIGBEE_CHANNEL_REGISTRY.register(general.OnOffConfiguration.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.OnOffConfiguration.cluster_id) class OnOffConfiguration(ZigbeeChannel): """OnOff Configuration channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(general.Ota.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Ota.cluster_id) class Ota(ZigbeeChannel): """OTA Channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(general.Partition.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Partition.cluster_id) class Partition(ZigbeeChannel): """Partition channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(general.PollControl.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PollControl.cluster_id) class PollControl(ZigbeeChannel): """Poll Control channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(general.PowerConfiguration.cluster_id) +@registries.DEVICE_TRACKER_CLUSTERS.register(general.PowerConfiguration.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerConfiguration.cluster_id) class PowerConfigurationChannel(ZigbeeChannel): """Channel for the zigbee power configuration cluster.""" @@ -373,28 +379,29 @@ class PowerConfigurationChannel(ZigbeeChannel): await self.get_attribute_value("battery_quantity", from_cache=from_cache) -@ZIGBEE_CHANNEL_REGISTRY.register(general.PowerProfile.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerProfile.cluster_id) class PowerProfile(ZigbeeChannel): """Power Profile channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(general.RSSILocation.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.RSSILocation.cluster_id) class RSSILocation(ZigbeeChannel): """RSSI Location channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(general.Scenes.cluster_id) +@registries.OUTPUT_CHANNEL_ONLY_CLUSTERS.register(general.Scenes.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Scenes.cluster_id) class Scenes(ZigbeeChannel): """Scenes channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(general.Time.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Time.cluster_id) class Time(ZigbeeChannel): """Time channel.""" diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index e3974d69d84..198eec67a46 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -10,7 +10,8 @@ import zigpy.zcl.clusters.homeautomation as homeautomation from homeassistant.helpers.dispatcher import async_dispatcher_send -from . import ZIGBEE_CHANNEL_REGISTRY, AttributeListeningChannel, ZigbeeChannel +from . import AttributeListeningChannel, ZigbeeChannel +from .. import registries from ..const import ( CHANNEL_ELECTRICAL_MEASUREMENT, REPORT_CONFIG_DEFAULT, @@ -20,39 +21,48 @@ from ..const import ( _LOGGER = logging.getLogger(__name__) -@ZIGBEE_CHANNEL_REGISTRY.register(homeautomation.ApplianceEventAlerts.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + homeautomation.ApplianceEventAlerts.cluster_id +) class ApplianceEventAlerts(ZigbeeChannel): """Appliance Event Alerts channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(homeautomation.ApplianceIdentification.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + homeautomation.ApplianceIdentification.cluster_id +) class ApplianceIdentification(ZigbeeChannel): """Appliance Identification channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(homeautomation.ApplianceStatistics.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + homeautomation.ApplianceStatistics.cluster_id +) class ApplianceStatistics(ZigbeeChannel): """Appliance Statistics channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(homeautomation.Diagnostic.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(homeautomation.Diagnostic.cluster_id) class Diagnostic(ZigbeeChannel): """Diagnostic channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(homeautomation.ElectricalMeasurement.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + homeautomation.ElectricalMeasurement.cluster_id +) class ElectricalMeasurementChannel(AttributeListeningChannel): """Channel that polls active power level.""" CHANNEL_NAME = CHANNEL_ELECTRICAL_MEASUREMENT + REPORT_CONFIG = ({"attr": "active_power", "config": REPORT_CONFIG_DEFAULT},) async def async_update(self): @@ -73,7 +83,9 @@ class ElectricalMeasurementChannel(AttributeListeningChannel): await super().async_initialize(from_cache) -@ZIGBEE_CHANNEL_REGISTRY.register(homeautomation.MeterIdentification.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + homeautomation.MeterIdentification.cluster_id +) class MeterIdentification(ZigbeeChannel): """Metering Identification channel.""" diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 2c115fd1118..46d9ffb52e5 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -11,24 +11,26 @@ import zigpy.zcl.clusters.hvac as hvac from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from . import ZIGBEE_CHANNEL_REGISTRY, ZigbeeChannel +from . import ZigbeeChannel +from .. import registries from ..const import REPORT_CONFIG_OP, SIGNAL_ATTR_UPDATED _LOGGER = logging.getLogger(__name__) -@ZIGBEE_CHANNEL_REGISTRY.register(hvac.Dehumidification.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.Dehumidification.cluster_id) class Dehumidification(ZigbeeChannel): """Dehumidification channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(hvac.Fan.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.Fan.cluster_id) class FanChannel(ZigbeeChannel): """Fan channel.""" _value_attribute = 0 + REPORT_CONFIG = ({"attr": "fan_mode", "config": REPORT_CONFIG_OP},) async def async_set_speed(self, value) -> None: @@ -71,21 +73,21 @@ class FanChannel(ZigbeeChannel): await super().async_initialize(from_cache) -@ZIGBEE_CHANNEL_REGISTRY.register(hvac.Pump.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.Pump.cluster_id) class Pump(ZigbeeChannel): """Pump channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(hvac.Thermostat.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.Thermostat.cluster_id) class Thermostat(ZigbeeChannel): """Thermostat channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(hvac.UserInterface.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.UserInterface.cluster_id) class UserInterface(ZigbeeChannel): """User interface (thermostat) channel.""" diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index 6448ea0d163..d8f769a3e24 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -8,20 +8,24 @@ import logging import zigpy.zcl.clusters.lighting as lighting -from . import ZIGBEE_CHANNEL_REGISTRY, ZigbeeChannel +from . import ZigbeeChannel +from .. import registries from ..const import REPORT_CONFIG_DEFAULT _LOGGER = logging.getLogger(__name__) -@ZIGBEE_CHANNEL_REGISTRY.register(lighting.Ballast.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(lighting.Ballast.cluster_id) class Ballast(ZigbeeChannel): """Ballast channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(lighting.Color.cluster_id) +@registries.BINDABLE_CLUSTERS.register(lighting.Color.cluster_id) +@registries.EVENT_RELAY_CLUSTERS.register(lighting.Color.cluster_id) +@registries.LIGHT_CLUSTERS.register(lighting.Color.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(lighting.Color.cluster_id) class ColorChannel(ZigbeeChannel): """Color channel.""" diff --git a/homeassistant/components/zha/core/channels/lightlink.py b/homeassistant/components/zha/core/channels/lightlink.py index a1308160b0f..99fed7d5d68 100644 --- a/homeassistant/components/zha/core/channels/lightlink.py +++ b/homeassistant/components/zha/core/channels/lightlink.py @@ -8,12 +8,14 @@ import logging import zigpy.zcl.clusters.lightlink as lightlink -from . import ZIGBEE_CHANNEL_REGISTRY, ZigbeeChannel +from . import ZigbeeChannel +from .. import registries _LOGGER = logging.getLogger(__name__) -@ZIGBEE_CHANNEL_REGISTRY.register(lightlink.LightLink.cluster_id) +@registries.CHANNEL_ONLY_CLUSTERS.register(lightlink.LightLink.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(lightlink.LightLink.cluster_id) class LightLink(ZigbeeChannel): """Lightlink channel.""" diff --git a/homeassistant/components/zha/core/channels/measurement.py b/homeassistant/components/zha/core/channels/measurement.py index 7de9ffdb54c..94d885592eb 100644 --- a/homeassistant/components/zha/core/channels/measurement.py +++ b/homeassistant/components/zha/core/channels/measurement.py @@ -8,7 +8,8 @@ import logging import zigpy.zcl.clusters.measurement as measurement -from . import ZIGBEE_CHANNEL_REGISTRY, AttributeListeningChannel +from . import AttributeListeningChannel +from .. import registries from ..const import ( REPORT_CONFIG_DEFAULT, REPORT_CONFIG_IMMEDIATE, @@ -19,42 +20,47 @@ from ..const import ( _LOGGER = logging.getLogger(__name__) -@ZIGBEE_CHANNEL_REGISTRY.register(measurement.FlowMeasurement.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.FlowMeasurement.cluster_id) class FlowMeasurement(AttributeListeningChannel): """Flow Measurement channel.""" REPORT_CONFIG = [{"attr": "measured_value", "config": REPORT_CONFIG_DEFAULT}] -@ZIGBEE_CHANNEL_REGISTRY.register(measurement.IlluminanceLevelSensing.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + measurement.IlluminanceLevelSensing.cluster_id +) class IlluminanceLevelSensing(AttributeListeningChannel): """Illuminance Level Sensing channel.""" REPORT_CONFIG = [{"attr": "level_status", "config": REPORT_CONFIG_DEFAULT}] -@ZIGBEE_CHANNEL_REGISTRY.register(measurement.IlluminanceMeasurement.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + measurement.IlluminanceMeasurement.cluster_id +) class IlluminanceMeasurement(AttributeListeningChannel): """Illuminance Measurement channel.""" REPORT_CONFIG = [{"attr": "measured_value", "config": REPORT_CONFIG_DEFAULT}] -@ZIGBEE_CHANNEL_REGISTRY.register(measurement.OccupancySensing.cluster_id) +@registries.BINARY_SENSOR_CLUSTERS.register(measurement.OccupancySensing.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.OccupancySensing.cluster_id) class OccupancySensing(AttributeListeningChannel): """Occupancy Sensing channel.""" REPORT_CONFIG = [{"attr": "occupancy", "config": REPORT_CONFIG_IMMEDIATE}] -@ZIGBEE_CHANNEL_REGISTRY.register(measurement.PressureMeasurement.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.PressureMeasurement.cluster_id) class PressureMeasurement(AttributeListeningChannel): """Pressure measurement channel.""" REPORT_CONFIG = [{"attr": "measured_value", "config": REPORT_CONFIG_DEFAULT}] -@ZIGBEE_CHANNEL_REGISTRY.register(measurement.RelativeHumidity.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.RelativeHumidity.cluster_id) class RelativeHumidity(AttributeListeningChannel): """Relative Humidity measurement channel.""" @@ -66,7 +72,9 @@ class RelativeHumidity(AttributeListeningChannel): ] -@ZIGBEE_CHANNEL_REGISTRY.register(measurement.TemperatureMeasurement.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + measurement.TemperatureMeasurement.cluster_id +) class TemperatureMeasurement(AttributeListeningChannel): """Temperature measurement channel.""" diff --git a/homeassistant/components/zha/core/channels/protocol.py b/homeassistant/components/zha/core/channels/protocol.py index 8918cf90211..b9785068f21 100644 --- a/homeassistant/components/zha/core/channels/protocol.py +++ b/homeassistant/components/zha/core/channels/protocol.py @@ -8,145 +8,154 @@ import logging import zigpy.zcl.clusters.protocol as protocol -from ..channels import ZIGBEE_CHANNEL_REGISTRY, ZigbeeChannel +from .. import registries +from ..channels import ZigbeeChannel _LOGGER = logging.getLogger(__name__) -@ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogInputExtended.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogInputExtended.cluster_id) class AnalogInputExtended(ZigbeeChannel): """Analog Input Extended channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogInputRegular.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogInputRegular.cluster_id) class AnalogInputRegular(ZigbeeChannel): """Analog Input Regular channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogOutputExtended.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogOutputExtended.cluster_id) class AnalogOutputExtended(ZigbeeChannel): """Analog Output Regular channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogOutputRegular.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogOutputRegular.cluster_id) class AnalogOutputRegular(ZigbeeChannel): """Analog Output Regular channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogValueExtended.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogValueExtended.cluster_id) class AnalogValueExtended(ZigbeeChannel): """Analog Value Extended edition channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogValueRegular.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogValueRegular.cluster_id) class AnalogValueRegular(ZigbeeChannel): """Analog Value Regular channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(protocol.BacnetProtocolTunnel.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BacnetProtocolTunnel.cluster_id) class BacnetProtocolTunnel(ZigbeeChannel): """Bacnet Protocol Tunnel channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryInputExtended.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryInputExtended.cluster_id) class BinaryInputExtended(ZigbeeChannel): """Binary Input Extended channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryInputRegular.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryInputRegular.cluster_id) class BinaryInputRegular(ZigbeeChannel): """Binary Input Regular channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryOutputExtended.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryOutputExtended.cluster_id) class BinaryOutputExtended(ZigbeeChannel): """Binary Output Extended channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryOutputRegular.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryOutputRegular.cluster_id) class BinaryOutputRegular(ZigbeeChannel): """Binary Output Regular channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryValueExtended.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryValueExtended.cluster_id) class BinaryValueExtended(ZigbeeChannel): """Binary Value Extended channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryValueRegular.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryValueRegular.cluster_id) class BinaryValueRegular(ZigbeeChannel): """Binary Value Regular channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(protocol.GenericTunnel.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.GenericTunnel.cluster_id) class GenericTunnel(ZigbeeChannel): """Generic Tunnel channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(protocol.MultistateInputExtended.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + protocol.MultistateInputExtended.cluster_id +) class MultiStateInputExtended(ZigbeeChannel): """Multistate Input Extended channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(protocol.MultistateInputRegular.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.MultistateInputRegular.cluster_id) class MultiStateInputRegular(ZigbeeChannel): """Multistate Input Regular channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(protocol.MultistateOutputExtended.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + protocol.MultistateOutputExtended.cluster_id +) class MultiStateOutputExtended(ZigbeeChannel): """Multistate Output Extended channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(protocol.MultistateOutputRegular.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + protocol.MultistateOutputRegular.cluster_id +) class MultiStateOutputRegular(ZigbeeChannel): """Multistate Output Regular channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(protocol.MultistateValueExtended.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + protocol.MultistateValueExtended.cluster_id +) class MultiStateValueExtended(ZigbeeChannel): """Multistate Value Extended channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(protocol.MultistateValueRegular.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.MultistateValueRegular.cluster_id) class MultiStateValueRegular(ZigbeeChannel): """Multistate Value Regular channel.""" diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 251b16a20df..c9485488d1c 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -11,27 +11,29 @@ import zigpy.zcl.clusters.security as security from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from . import ZIGBEE_CHANNEL_REGISTRY, ZigbeeChannel +from . import ZigbeeChannel +from .. import registries from ..const import SIGNAL_ATTR_UPDATED _LOGGER = logging.getLogger(__name__) -@ZIGBEE_CHANNEL_REGISTRY.register(security.IasAce.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(security.IasAce.cluster_id) class IasAce(ZigbeeChannel): """IAS Ancillary Control Equipment channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(security.IasWd.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(security.IasWd.cluster_id) class IasWd(ZigbeeChannel): """IAS Warning Device channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(security.IasZone.cluster_id) +@registries.BINARY_SENSOR_CLUSTERS.register(security.IasZone.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(security.IasZone.cluster_id) class IASZoneChannel(ZigbeeChannel): """Channel for the IASZone Zigbee cluster.""" diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 7ab850da09b..a182193caba 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -8,90 +8,91 @@ import logging import zigpy.zcl.clusters.smartenergy as smartenergy -from ..channels import ZIGBEE_CHANNEL_REGISTRY, AttributeListeningChannel, ZigbeeChannel +from .. import registries +from ..channels import AttributeListeningChannel, ZigbeeChannel from ..const import REPORT_CONFIG_DEFAULT _LOGGER = logging.getLogger(__name__) -@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Calendar.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Calendar.cluster_id) class Calendar(ZigbeeChannel): """Calendar channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.DeviceManagement.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.DeviceManagement.cluster_id) class DeviceManagement(ZigbeeChannel): """Device Management channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Drlc.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Drlc.cluster_id) class Drlc(ZigbeeChannel): """Demand Response and Load Control channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.EnergyManagement.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.EnergyManagement.cluster_id) class EnergyManagement(ZigbeeChannel): """Energy Management channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Events.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Events.cluster_id) class Events(ZigbeeChannel): """Event channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.KeyEstablishment.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.KeyEstablishment.cluster_id) class KeyEstablishment(ZigbeeChannel): """Key Establishment channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.MduPairing.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.MduPairing.cluster_id) class MduPairing(ZigbeeChannel): """Pairing channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Messaging.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Messaging.cluster_id) class Messaging(ZigbeeChannel): """Messaging channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Metering.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Metering.cluster_id) class Metering(AttributeListeningChannel): """Metering channel.""" REPORT_CONFIG = [{"attr": "instantaneous_demand", "config": REPORT_CONFIG_DEFAULT}] -@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Prepayment.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Prepayment.cluster_id) class Prepayment(ZigbeeChannel): """Prepayment channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Price.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Price.cluster_id) class Price(ZigbeeChannel): """Price channel.""" pass -@ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Tunneling.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Tunneling.cluster_id) class Tunneling(ZigbeeChannel): """Tunneling channel.""" diff --git a/homeassistant/components/zha/core/decorators.py b/homeassistant/components/zha/core/decorators.py new file mode 100644 index 00000000000..4148cff6ca9 --- /dev/null +++ b/homeassistant/components/zha/core/decorators.py @@ -0,0 +1,37 @@ +"""Decorators for ZHA core registries.""" +from typing import Callable, TypeVar, Union + +CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # noqa pylint: disable=invalid-name + + +class DictRegistry(dict): + """Dict Registry of items.""" + + def register( + self, name: Union[int, str], item: Union[str, CALLABLE_T] = None + ) -> Callable[[CALLABLE_T], CALLABLE_T]: + """Return decorator to register item with a specific name.""" + + def decorator(channel: CALLABLE_T) -> CALLABLE_T: + """Register decorated channel or item.""" + if item is None: + self[name] = channel + else: + self[name] = item + return channel + + return decorator + + +class SetRegistry(set): + """Set Registry of items.""" + + def register(self, name: Union[int, str]) -> Callable[[CALLABLE_T], CALLABLE_T]: + """Return decorator to register item with a specific name.""" + + def decorator(channel: CALLABLE_T) -> CALLABLE_T: + """Register decorated channel or item.""" + self.add(name) + return channel + + return decorator diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 9936a3451c8..c4489164b0c 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -13,12 +13,7 @@ from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from .channels import ( - ZIGBEE_CHANNEL_REGISTRY, - AttributeListeningChannel, - EventRelayChannel, - ZDOChannel, -) +from .channels import AttributeListeningChannel, EventRelayChannel, ZDOChannel from .const import ( COMPONENTS, CONF_DEVICE_CONFIG, @@ -39,6 +34,7 @@ from .registries import ( SENSOR_TYPES, SINGLE_INPUT_CLUSTER_DEVICE_CLASS, SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, + ZIGBEE_CHANNEL_REGISTRY, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 3899d601017..37bc6c7a2c1 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -122,13 +122,12 @@ def async_is_bindable_target(source_zha_device, target_zha_device): source_clusters = source_zha_device.async_get_std_clusters() target_clusters = target_zha_device.async_get_std_clusters() - bindables = set(BINDABLE_CLUSTERS) for endpoint_id in source_clusters: for t_endpoint_id in target_clusters: matches = set( source_clusters[endpoint_id][CLUSTER_TYPE_OUT].keys() ).intersection(target_clusters[t_endpoint_id][CLUSTER_TYPE_IN].keys()) - if any(bindable in bindables for bindable in matches): + if any(bindable in BINDABLE_CLUSTERS for bindable in matches): return True return False diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 757a6c3e43f..09a25c701e9 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -4,6 +4,7 @@ Mapping registries for Zigbee Home Automation. For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ +import collections from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER @@ -33,24 +34,25 @@ from .const import ( ZONE, RadioType, ) +from .decorators import DictRegistry, SetRegistry -BINARY_SENSOR_CLUSTERS = set() +BINARY_SENSOR_CLUSTERS = SetRegistry() BINARY_SENSOR_TYPES = {} -BINDABLE_CLUSTERS = [] -CHANNEL_ONLY_CLUSTERS = [] +BINDABLE_CLUSTERS = SetRegistry() +CHANNEL_ONLY_CLUSTERS = SetRegistry() CLUSTER_REPORT_CONFIGS = {} CUSTOM_CLUSTER_MAPPINGS = {} -DEVICE_CLASS = {} -DEVICE_TRACKER_CLUSTERS = set() -EVENT_RELAY_CLUSTERS = [] -LIGHT_CLUSTERS = set() -OUTPUT_CHANNEL_ONLY_CLUSTERS = [] +DEVICE_CLASS = collections.defaultdict(dict) +DEVICE_TRACKER_CLUSTERS = SetRegistry() +EVENT_RELAY_CLUSTERS = SetRegistry() +LIGHT_CLUSTERS = SetRegistry() +OUTPUT_CHANNEL_ONLY_CLUSTERS = SetRegistry() RADIO_TYPES = {} -REMOTE_DEVICE_TYPES = {} +REMOTE_DEVICE_TYPES = collections.defaultdict(list) SENSOR_TYPES = {} SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {} SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {} -SWITCH_CLUSTERS = set() +SWITCH_CLUSTERS = SetRegistry() SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02 SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000 SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45 @@ -62,6 +64,11 @@ COMPONENT_CLUSTERS = { SWITCH: SWITCH_CLUSTERS, } +ZIGBEE_CHANNEL_REGISTRY = DictRegistry() + +# importing channels updates registries +from . import channels # noqa pylint: disable=wrong-import-position,unused-import + def establish_device_mappings(): """Establish mappings between ZCL objects and HA ZHA objects. @@ -72,16 +79,6 @@ def establish_device_mappings(): from zigpy import zcl from zigpy.profiles import zha, zll - if zha.PROFILE_ID not in DEVICE_CLASS: - DEVICE_CLASS[zha.PROFILE_ID] = {} - if zll.PROFILE_ID not in DEVICE_CLASS: - DEVICE_CLASS[zll.PROFILE_ID] = {} - - if zha.PROFILE_ID not in REMOTE_DEVICE_TYPES: - REMOTE_DEVICE_TYPES[zha.PROFILE_ID] = [] - if zll.PROFILE_ID not in REMOTE_DEVICE_TYPES: - REMOTE_DEVICE_TYPES[zll.PROFILE_ID] = [] - def get_ezsp_radio(): import bellows.ezsp from bellows.zigbee.application import ControllerApplication @@ -133,9 +130,6 @@ def establish_device_mappings(): } BINARY_SENSOR_CLUSTERS.add(SMARTTHINGS_ACCELERATION_CLUSTER) - BINARY_SENSOR_CLUSTERS.add(zcl.clusters.general.OnOff.cluster_id) - BINARY_SENSOR_CLUSTERS.add(zcl.clusters.measurement.OccupancySensing.cluster_id) - BINARY_SENSOR_CLUSTERS.add(zcl.clusters.security.IasZone.cluster_id) BINARY_SENSOR_TYPES.update( { @@ -146,13 +140,6 @@ def establish_device_mappings(): } ) - BINDABLE_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id) - BINDABLE_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id) - BINDABLE_CLUSTERS.append(zcl.clusters.lighting.Color.cluster_id) - - CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.general.Basic.cluster_id) - CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.lightlink.LightLink.cluster_id) - CLUSTER_REPORT_CONFIGS.update( { SMARTTHINGS_ACCELERATION_CLUSTER: [ @@ -200,17 +187,6 @@ def establish_device_mappings(): } ) - DEVICE_TRACKER_CLUSTERS.add(zcl.clusters.general.PowerConfiguration.cluster_id) - - EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id) - EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id) - - OUTPUT_CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.general.Scenes.cluster_id) - - LIGHT_CLUSTERS.add(zcl.clusters.general.LevelControl.cluster_id) - LIGHT_CLUSTERS.add(zcl.clusters.general.OnOff.cluster_id) - LIGHT_CLUSTERS.add(zcl.clusters.lighting.Color.cluster_id) - SINGLE_INPUT_CLUSTER_DEVICE_CLASS.update( { # this works for now but if we hit conflicts we can break it out to @@ -251,8 +227,6 @@ def establish_device_mappings(): } ) - SWITCH_CLUSTERS.add(zcl.clusters.general.OnOff.cluster_id) - zhap = zha.PROFILE_ID REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_CONTROLLER) REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_DIMMER_SWITCH) diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index e5d50060279..3be3aaf0930 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -1,10 +1,10 @@ """Test ZHA Core channels.""" -import homeassistant.components.zha.core.channels import pytest import zigpy.types as t import homeassistant.components.zha.core.channels as channels import homeassistant.components.zha.core.device as zha_device +import homeassistant.components.zha.core.registries as registries from .common import make_device @@ -77,7 +77,7 @@ async def test_in_channel_config(cluster_id, bind_count, attrs, zha_gateway, has zha_dev = zha_device.ZHADevice(hass, zigpy_dev, zha_gateway) cluster = zigpy_dev.endpoints[1].in_clusters[cluster_id] - channel_class = channels.ZIGBEE_CHANNEL_REGISTRY.get( + channel_class = registries.ZIGBEE_CHANNEL_REGISTRY.get( cluster_id, channels.AttributeListeningChannel ) channel = channel_class(cluster, zha_dev) @@ -136,7 +136,7 @@ async def test_out_channel_config(cluster_id, bind_count, zha_gateway, hass): cluster = zigpy_dev.endpoints[1].out_clusters[cluster_id] cluster.bind_only = True - channel_class = homeassistant.components.zha.core.channels.ZIGBEE_CHANNEL_REGISTRY.get( + channel_class = registries.ZIGBEE_CHANNEL_REGISTRY.get( cluster_id, channels.AttributeListeningChannel ) channel = channel_class(cluster, zha_dev) @@ -149,7 +149,7 @@ async def test_out_channel_config(cluster_id, bind_count, zha_gateway, hass): def test_channel_registry(): """Test ZIGBEE Channel Registry.""" - for cluster_id, channel in channels.ZIGBEE_CHANNEL_REGISTRY.items(): + for (cluster_id, channel) in registries.ZIGBEE_CHANNEL_REGISTRY.items(): assert isinstance(cluster_id, int) assert 0 <= cluster_id <= 0xFFFF assert issubclass(channel, channels.ZigbeeChannel) From 506350da116e67b92416487fe89db5640f2bf0a4 Mon Sep 17 00:00:00 2001 From: eyager1 <44526531+eyager1@users.noreply.github.com> Date: Tue, 6 Aug 2019 20:24:17 -0400 Subject: [PATCH 069/273] Implement "Aux Heat" support for Zwave Climate (#25694) * Remove "Aux Heat" from Zwave climate mappings * Implement Aux Heat support for zwave climate * Pylint fix. * Pylint fix 2nd try --- homeassistant/components/zwave/climate.py | 43 +++++++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index a9dfe4eec00..2c7ce4b18a4 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -19,6 +19,7 @@ from homeassistant.components.climate.const import ( HVAC_MODE_OFF, PRESET_BOOST, PRESET_NONE, + SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, @@ -39,8 +40,9 @@ REMOTEC = 0x5254 REMOTEC_ZXT_120 = 0x8377 REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120) ATTR_OPERATING_STATE = "operating_state" +ATTR_FAN_STATE = "fan_state" ATTR_FAN_ACTION = "fan_action" - +AUX_HEAT_ZWAVE_MODE = "Aux Heat" # Device is in manufacturer specific mode (e.g. setting the valve manually) PRESET_MANUFACTURER_SPECIFIC = "Manufacturer Specific" @@ -54,7 +56,6 @@ HVAC_STATE_MAPPINGS = { "heat": HVAC_MODE_HEAT, "heat mode": HVAC_MODE_HEAT, "heat (default)": HVAC_MODE_HEAT, - "aux heat": HVAC_MODE_HEAT, "furnace": HVAC_MODE_HEAT, "fan only": HVAC_MODE_FAN_ONLY, "dry air": HVAC_MODE_DRY, @@ -62,6 +63,7 @@ HVAC_STATE_MAPPINGS = { "cool": HVAC_MODE_COOL, "heat_cool": HVAC_MODE_HEAT_COOL, "auto": HVAC_MODE_HEAT_COOL, + "auto changeover": HVAC_MODE_HEAT_COOL, } HVAC_CURRENT_MAPPINGS = { @@ -127,6 +129,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): self._hvac_list = None # [zwave_mode] self._hvac_mapping = None # {ha_mode:zwave_mode} self._hvac_mode = None # ha_mode + self._aux_heat = None self._default_hvac_mode = None # ha_mode self._preset_mapping = None # {ha_mode:zwave_mode} self._preset_list = None # [zwave_mode] @@ -159,6 +162,8 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): support |= SUPPORT_FAN_MODE if self._zxt_120 == 1 and self.values.zxt_120_swing_mode: support |= SUPPORT_SWING_MODE + if self._aux_heat: + support |= SUPPORT_AUX_HEAT if self._preset_list: support |= SUPPORT_PRESET_MODE return support @@ -177,7 +182,10 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): for mode in mode_list: ha_mode = HVAC_STATE_MAPPINGS.get(str(mode).lower()) ha_preset = PRESET_MAPPINGS.get(str(mode).lower()) - if ha_mode and ha_mode not in self._hvac_mapping: + if mode == AUX_HEAT_ZWAVE_MODE: + # Aux Heat should not be included in any mapping + self._aux_heat = True + elif ha_mode and ha_mode not in self._hvac_mapping: self._hvac_mapping[ha_mode] = mode self._hvac_list.append(ha_mode) elif ha_preset and ha_preset not in self._preset_mapping: @@ -246,6 +254,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): _LOGGER.debug("self._hvac_mode=%s", self._hvac_mode) _LOGGER.debug("self._default_hvac_mode=%s", self._default_hvac_mode) _LOGGER.debug("self._hvac_action=%s", self._hvac_action) + _LOGGER.debug("self._aux_heat=%s", self._aux_heat) _LOGGER.debug("self._preset_mapping=%s", self._preset_mapping) _LOGGER.debug("self._preset_list=%s", self._preset_list) _LOGGER.debug("self._preset_mode=%s", self._preset_mode) @@ -356,6 +365,15 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): """ return self._hvac_action + @property + def is_aux_heat(self): + """Return true if aux heater.""" + if not self._aux_heat: + return None + if self.values.mode.data == AUX_HEAT_ZWAVE_MODE: + return True + return False + @property def preset_mode(self): """Return preset operation ie. eco, away. @@ -404,6 +422,25 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): _LOGGER.debug("Set operation_mode to %s", operation_mode) self.values.mode.data = operation_mode + def turn_aux_heat_on(self): + """Turn auxillary heater on.""" + if not self._aux_heat: + return + operation_mode = AUX_HEAT_ZWAVE_MODE + _LOGGER.debug("Aux heat on. Set operation mode to %s", operation_mode) + self.values.mode.data = operation_mode + + def turn_aux_heat_off(self): + """Turn auxillary heater off.""" + if not self._aux_heat: + return + if HVAC_MODE_HEAT in self._hvac_mapping: + operation_mode = self._hvac_mapping.get(HVAC_MODE_HEAT) + else: + operation_mode = self._hvac_mapping.get(HVAC_MODE_OFF) + _LOGGER.debug("Aux heat off. Set operation mode to %s", operation_mode) + self.values.mode.data = operation_mode + def set_preset_mode(self, preset_mode): """Set new target preset mode.""" _LOGGER.debug("Set preset_mode to %s", preset_mode) From e0be4efe3eb99fb731b622268dfa11e7d7cd22d4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 7 Aug 2019 02:32:15 +0200 Subject: [PATCH 070/273] Add test case to identify missing MQTT configuration abbreviations (#25616) * Add missing abbreviations * Move abbreviations to own file, add script to find missing abbreviations * Move check from script to test case * Lint * Rewrite to use pathlib * Lint --- .../components/mqtt/abbreviations.py | 180 ++++++++++++++++++ homeassistant/components/mqtt/discovery.py | 145 +------------- tests/components/mqtt/test_discovery.py | 60 ++++++ 3 files changed, 241 insertions(+), 144 deletions(-) create mode 100644 homeassistant/components/mqtt/abbreviations.py diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py new file mode 100644 index 00000000000..2350dfc6634 --- /dev/null +++ b/homeassistant/components/mqtt/abbreviations.py @@ -0,0 +1,180 @@ +"""Abbreviations supported by MQTT discovery.""" + +ABBREVIATIONS = { + "act_t": "action_topic", + "act_tpl": "action_template", + "aux_cmd_t": "aux_command_topic", + "aux_stat_tpl": "aux_state_template", + "aux_stat_t": "aux_state_topic", + "avty_t": "availability_topic", + "away_mode_cmd_t": "away_mode_command_topic", + "away_mode_stat_tpl": "away_mode_state_template", + "away_mode_stat_t": "away_mode_state_topic", + "b_tpl": "blue_template", + "bri_cmd_t": "brightness_command_topic", + "bri_scl": "brightness_scale", + "bri_stat_t": "brightness_state_topic", + "bri_tpl": "brightness_template", + "bri_val_tpl": "brightness_value_template", + "clr_temp_cmd_tpl": "color_temp_command_template", + "bat_lev_t": "battery_level_topic", + "bat_lev_tpl": "battery_level_template", + "chrg_t": "charging_topic", + "chrg_tpl": "charging_template", + "clr_temp_cmd_t": "color_temp_command_topic", + "clr_temp_stat_t": "color_temp_state_topic", + "clr_temp_tpl": "color_temp_template", + "clr_temp_val_tpl": "color_temp_value_template", + "cln_t": "cleaning_topic", + "cln_tpl": "cleaning_template", + "cmd_off_tpl": "command_off_template", + "cmd_on_tpl": "command_on_template", + "cmd_t": "command_topic", + "cmd_tpl": "command_template", + "cod_arm_req": "code_arm_required", + "cod_dis_req": "code_disarm_required", + "curr_temp_t": "current_temperature_topic", + "curr_temp_tpl": "current_temperature_template", + "dev": "device", + "dev_cla": "device_class", + "dock_t": "docked_topic", + "dock_tpl": "docked_template", + "err_t": "error_topic", + "err_tpl": "error_template", + "fanspd_t": "fan_speed_topic", + "fanspd_tpl": "fan_speed_template", + "fanspd_lst": "fan_speed_list", + "flsh_tlng": "flash_time_long", + "flsh_tsht": "flash_time_short", + "fx_cmd_t": "effect_command_topic", + "fx_list": "effect_list", + "fx_stat_t": "effect_state_topic", + "fx_tpl": "effect_template", + "fx_val_tpl": "effect_value_template", + "exp_aft": "expire_after", + "fan_mode_cmd_t": "fan_mode_command_topic", + "fan_mode_stat_tpl": "fan_mode_state_template", + "fan_mode_stat_t": "fan_mode_state_topic", + "frc_upd": "force_update", + "g_tpl": "green_template", + "hold_cmd_t": "hold_command_topic", + "hold_stat_tpl": "hold_state_template", + "hold_stat_t": "hold_state_topic", + "hs_cmd_t": "hs_command_topic", + "hs_stat_t": "hs_state_topic", + "hs_val_tpl": "hs_value_template", + "ic": "icon", + "init": "initial", + "json_attr": "json_attributes", + "json_attr_t": "json_attributes_topic", + "json_attr_tpl": "json_attributes_template", + "max_temp": "max_temp", + "min_temp": "min_temp", + "mode_cmd_t": "mode_command_topic", + "mode_stat_tpl": "mode_state_template", + "mode_stat_t": "mode_state_topic", + "name": "name", + "off_dly": "off_delay", + "on_cmd_type": "on_command_type", + "opt": "optimistic", + "osc_cmd_t": "oscillation_command_topic", + "osc_stat_t": "oscillation_state_topic", + "osc_val_tpl": "oscillation_value_template", + "pl_arm_away": "payload_arm_away", + "pl_arm_home": "payload_arm_home", + "pl_arm_nite": "payload_arm_night", + "pl_avail": "payload_available", + "pl_cln_sp": "payload_clean_spot", + "pl_cls": "payload_close", + "pl_disarm": "payload_disarm", + "pl_hi_spd": "payload_high_speed", + "pl_lock": "payload_lock", + "pl_loc": "payload_locate", + "pl_lo_spd": "payload_low_speed", + "pl_med_spd": "payload_medium_speed", + "pl_not_avail": "payload_not_available", + "pl_off": "payload_off", + "pl_off_spd": "payload_off_speed", + "pl_on": "payload_on", + "pl_open": "payload_open", + "pl_osc_off": "payload_oscillation_off", + "pl_osc_on": "payload_oscillation_on", + "pl_paus": "payload_pause", + "pl_stop": "payload_stop", + "pl_strt": "payload_start", + "pl_stpa": "payload_start_pause", + "pl_ret": "payload_return_to_base", + "pl_toff": "payload_turn_off", + "pl_ton": "payload_turn_on", + "pl_unlk": "payload_unlock", + "pos_clsd": "position_closed", + "pos_open": "position_open", + "pow_cmd_t": "power_command_topic", + "pow_stat_t": "power_state_topic", + "pow_stat_tpl": "power_state_template", + "r_tpl": "red_template", + "ret": "retain", + "rgb_cmd_tpl": "rgb_command_template", + "rgb_cmd_t": "rgb_command_topic", + "rgb_stat_t": "rgb_state_topic", + "rgb_val_tpl": "rgb_value_template", + "send_cmd_t": "send_command_topic", + "send_if_off": "send_if_off", + "set_fan_spd_t": "set_fan_speed_topic", + "set_pos_tpl": "set_position_template", + "set_pos_t": "set_position_topic", + "pos_t": "position_topic", + "spd_cmd_t": "speed_command_topic", + "spd_stat_t": "speed_state_topic", + "spd_val_tpl": "speed_value_template", + "spds": "speeds", + "stat_clsd": "state_closed", + "stat_off": "state_off", + "stat_on": "state_on", + "stat_open": "state_open", + "stat_t": "state_topic", + "stat_tpl": "state_template", + "stat_val_tpl": "state_value_template", + "sup_feat": "supported_features", + "swing_mode_cmd_t": "swing_mode_command_topic", + "swing_mode_stat_tpl": "swing_mode_state_template", + "swing_mode_stat_t": "swing_mode_state_topic", + "temp_cmd_t": "temperature_command_topic", + "temp_hi_cmd_t": "temperature_high_command_topic", + "temp_hi_stat_tpl": "temperature_high_state_template", + "temp_hi_stat_t": "temperature_high_state_topic", + "temp_lo_cmd_t": "temperature_low_command_topic", + "temp_lo_stat_tpl": "temperature_low_state_template", + "temp_lo_stat_t": "temperature_low_state_topic", + "temp_stat_tpl": "temperature_state_template", + "temp_stat_t": "temperature_state_topic", + "tilt_clsd_val": "tilt_closed_value", + "tilt_cmd_t": "tilt_command_topic", + "tilt_inv_stat": "tilt_invert_state", + "tilt_max": "tilt_max", + "tilt_min": "tilt_min", + "tilt_opnd_val": "tilt_opened_value", + "tilt_opt": "tilt_optimistic", + "tilt_status_t": "tilt_status_topic", + "tilt_status_tpl": "tilt_status_template", + "t": "topic", + "uniq_id": "unique_id", + "unit_of_meas": "unit_of_measurement", + "val_tpl": "value_template", + "whit_val_cmd_t": "white_value_command_topic", + "whit_val_scl": "white_value_scale", + "whit_val_stat_t": "white_value_state_topic", + "whit_val_tpl": "white_value_template", + "xy_cmd_t": "xy_command_topic", + "xy_stat_t": "xy_state_topic", + "xy_val_tpl": "xy_value_template", +} + +DEVICE_ABBREVIATIONS = { + "cns": "connections", + "ids": "identifiers", + "name": "name", + "mf": "manufacturer", + "mdl": "model", + "sw": "sw_version", +} diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 1d307599698..d611b8db13e 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -10,6 +10,7 @@ from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import HomeAssistantType +from .abbreviations import ABBREVIATIONS, DEVICE_ABBREVIATIONS from .const import ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC _LOGGER = logging.getLogger(__name__) @@ -64,150 +65,6 @@ MQTT_DISCOVERY_NEW = "mqtt_discovery_new_{}_{}" TOPIC_BASE = "~" -ABBREVIATIONS = { - "aux_cmd_t": "aux_command_topic", - "aux_stat_tpl": "aux_state_template", - "aux_stat_t": "aux_state_topic", - "avty_t": "availability_topic", - "away_mode_cmd_t": "away_mode_command_topic", - "away_mode_stat_tpl": "away_mode_state_template", - "away_mode_stat_t": "away_mode_state_topic", - "b_tpl": "blue_template", - "bri_cmd_t": "brightness_command_topic", - "bri_scl": "brightness_scale", - "bri_stat_t": "brightness_state_topic", - "bri_tpl": "brightness_template", - "bri_val_tpl": "brightness_value_template", - "clr_temp_cmd_tpl": "color_temp_command_template", - "bat_lev_t": "battery_level_topic", - "bat_lev_tpl": "battery_level_template", - "chrg_t": "charging_topic", - "chrg_tpl": "charging_template", - "clr_temp_cmd_t": "color_temp_command_topic", - "clr_temp_stat_t": "color_temp_state_topic", - "clr_temp_val_tpl": "color_temp_value_template", - "cln_t": "cleaning_topic", - "cln_tpl": "cleaning_template", - "cmd_off_tpl": "command_off_template", - "cmd_on_tpl": "command_on_template", - "cmd_t": "command_topic", - "curr_temp_t": "current_temperature_topic", - "curr_temp_tpl": "current_temperature_template", - "dev": "device", - "dev_cla": "device_class", - "dock_t": "docked_topic", - "dock_tpl": "docked_template", - "err_t": "error_topic", - "err_tpl": "error_template", - "fanspd_t": "fan_speed_topic", - "fanspd_tpl": "fan_speed_template", - "fanspd_lst": "fan_speed_list", - "fx_cmd_t": "effect_command_topic", - "fx_list": "effect_list", - "fx_stat_t": "effect_state_topic", - "fx_tpl": "effect_template", - "fx_val_tpl": "effect_value_template", - "exp_aft": "expire_after", - "fan_mode_cmd_t": "fan_mode_command_topic", - "fan_mode_stat_tpl": "fan_mode_state_template", - "fan_mode_stat_t": "fan_mode_state_topic", - "frc_upd": "force_update", - "g_tpl": "green_template", - "hold_cmd_t": "hold_command_topic", - "hold_stat_tpl": "hold_state_template", - "hold_stat_t": "hold_state_topic", - "ic": "icon", - "init": "initial", - "json_attr": "json_attributes", - "json_attr_t": "json_attributes_topic", - "max_temp": "max_temp", - "min_temp": "min_temp", - "mode_cmd_t": "mode_command_topic", - "mode_stat_tpl": "mode_state_template", - "mode_stat_t": "mode_state_topic", - "name": "name", - "on_cmd_type": "on_command_type", - "opt": "optimistic", - "osc_cmd_t": "oscillation_command_topic", - "osc_stat_t": "oscillation_state_topic", - "osc_val_tpl": "oscillation_value_template", - "pl_arm_away": "payload_arm_away", - "pl_arm_home": "payload_arm_home", - "pl_avail": "payload_available", - "pl_cls": "payload_close", - "pl_disarm": "payload_disarm", - "pl_hi_spd": "payload_high_speed", - "pl_lock": "payload_lock", - "pl_lo_spd": "payload_low_speed", - "pl_med_spd": "payload_medium_speed", - "pl_not_avail": "payload_not_available", - "pl_off": "payload_off", - "pl_on": "payload_on", - "pl_open": "payload_open", - "pl_osc_off": "payload_oscillation_off", - "pl_osc_on": "payload_oscillation_on", - "pl_stop": "payload_stop", - "pl_unlk": "payload_unlock", - "pow_cmd_t": "power_command_topic", - "r_tpl": "red_template", - "ret": "retain", - "rgb_cmd_tpl": "rgb_command_template", - "rgb_cmd_t": "rgb_command_topic", - "rgb_stat_t": "rgb_state_topic", - "rgb_val_tpl": "rgb_value_template", - "send_cmd_t": "send_command_topic", - "send_if_off": "send_if_off", - "set_pos_tpl": "set_position_template", - "set_pos_t": "set_position_topic", - "pos_t": "position_topic", - "spd_cmd_t": "speed_command_topic", - "spd_stat_t": "speed_state_topic", - "spd_val_tpl": "speed_value_template", - "spds": "speeds", - "stat_clsd": "state_closed", - "stat_off": "state_off", - "stat_on": "state_on", - "stat_open": "state_open", - "stat_t": "state_topic", - "stat_tpl": "state_template", - "stat_val_tpl": "state_value_template", - "sup_feat": "supported_features", - "swing_mode_cmd_t": "swing_mode_command_topic", - "swing_mode_stat_tpl": "swing_mode_state_template", - "swing_mode_stat_t": "swing_mode_state_topic", - "temp_cmd_t": "temperature_command_topic", - "temp_stat_tpl": "temperature_state_template", - "temp_stat_t": "temperature_state_topic", - "tilt_clsd_val": "tilt_closed_value", - "tilt_cmd_t": "tilt_command_topic", - "tilt_inv_stat": "tilt_invert_state", - "tilt_max": "tilt_max", - "tilt_min": "tilt_min", - "tilt_opnd_val": "tilt_opened_value", - "tilt_status_opt": "tilt_status_optimistic", - "tilt_status_t": "tilt_status_topic", - "t": "topic", - "uniq_id": "unique_id", - "unit_of_meas": "unit_of_measurement", - "val_tpl": "value_template", - "whit_val_cmd_t": "white_value_command_topic", - "whit_val_scl": "white_value_scale", - "whit_val_stat_t": "white_value_state_topic", - "whit_val_tpl": "white_value_template", - "xy_cmd_t": "xy_command_topic", - "xy_stat_t": "xy_state_topic", - "xy_val_tpl": "xy_value_template", -} - -DEVICE_ABBREVIATIONS = { - "cns": "connections", - "ids": "identifiers", - "name": "name", - "mf": "manufacturer", - "mdl": "model", - "sw": "sw_version", -} - def clear_discovery_hash(hass, discovery_hash): """Clear entry in ALREADY_DISCOVERED list.""" diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index d88ab8c4f70..860ef52a98a 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1,7 +1,14 @@ """The tests for the MQTT discovery.""" +from pathlib import Path +import re + from unittest.mock import patch from homeassistant.components import mqtt +from homeassistant.components.mqtt.abbreviations import ( + ABBREVIATIONS, + DEVICE_ABBREVIATIONS, +) from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED, async_start from homeassistant.const import STATE_OFF, STATE_ON @@ -245,6 +252,59 @@ async def test_discovery_expansion(hass, mqtt_mock, caplog): assert state.state == STATE_ON +ABBREVIATIONS_WHITE_LIST = [ + # MQTT client/server settings + "CONF_BIRTH_MESSAGE", + "CONF_BROKER", + "CONF_CERTIFICATE", + "CONF_CLIENT_CERT", + "CONF_CLIENT_ID", + "CONF_CLIENT_KEY", + "CONF_DISCOVERY", + "CONF_DISCOVERY_PREFIX", + "CONF_EMBEDDED", + "CONF_KEEPALIVE", + "CONF_TLS_INSECURE", + "CONF_TLS_VERSION", + "CONF_WILL_MESSAGE", + # Undocumented device configuration + "CONF_DEPRECATED_VIA_HUB", + "CONF_VIA_DEVICE", + # Already short + "CONF_FAN_MODE_LIST", + "CONF_HOLD_LIST", + "CONF_HS", + "CONF_MODE_LIST", + "CONF_PRECISION", + "CONF_QOS", + "CONF_SCHEMA", + "CONF_SWING_MODE_LIST", + "CONF_TEMP_STEP", +] + + +async def test_missing_discover_abbreviations(hass, mqtt_mock, caplog): + """Check MQTT platforms for missing abbreviations.""" + missing = [] + regex = re.compile(r"(CONF_[a-zA-Z\d_]*) *= *[\'\"]([a-zA-Z\d_]*)[\'\"]") + for fil in Path(mqtt.__file__).parent.rglob("*.py"): + with open(fil) as file: + matches = re.findall(regex, file.read()) + for match in matches: + if ( + match[1] not in ABBREVIATIONS.values() + and match[1] not in DEVICE_ABBREVIATIONS.values() + and match[0] not in ABBREVIATIONS_WHITE_LIST + ): + missing.append( + "{}: no abbreviation for {} ({})".format( + fil, match[1], match[0] + ) + ) + + assert not missing + + async def test_implicit_state_topic_alarm(hass, mqtt_mock, caplog): """Test implicit state topic for alarm_control_panel.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) From c3c45f8198d8d30cc1bb79e80a74251c779d3cc8 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 6 Aug 2019 20:59:18 -0400 Subject: [PATCH 071/273] Manufacturer specific channel for SmartThings. (#25739) * Manufacturer specific channel for SmartThings. * Lint. --- .../zha/core/channels/manufacturerspecific.py | 31 +++++++++++++++++++ .../components/zha/core/registries.py | 20 ------------ 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index a0eebd78343..6ed9de9b303 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -6,4 +6,35 @@ https://home-assistant.io/components/zha/ """ import logging +from . import AttributeListeningChannel +from .. import registries +from ..const import REPORT_CONFIG_ASAP, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MIN_INT + + _LOGGER = logging.getLogger(__name__) + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(registries.SMARTTHINGS_HUMIDITY_CLUSTER) +class SmartThingsHumidity(AttributeListeningChannel): + """Smart Things Humidity channel.""" + + REPORT_CONFIG = [ + { + "attr": "measured_value", + "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), + } + ] + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + registries.SMARTTHINGS_ACCELERATION_CLUSTER +) +class SmartThingsAcceleration(AttributeListeningChannel): + """Smart Things Acceleration channel.""" + + REPORT_CONFIG = [ + {"attr": "acceleration", "config": REPORT_CONFIG_ASAP}, + {"attr": "x_axis", "config": REPORT_CONFIG_ASAP}, + {"attr": "y_axis", "config": REPORT_CONFIG_ASAP}, + {"attr": "z_axis", "config": REPORT_CONFIG_ASAP}, + ] diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 09a25c701e9..db7e89dce82 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -16,9 +16,6 @@ from homeassistant.components.switch import DOMAIN as SWITCH from .const import ( CONTROLLER, - REPORT_CONFIG_ASAP, - REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_MIN_INT, SENSOR_ACCELERATION, SENSOR_BATTERY, SENSOR_ELECTRICAL_MEASUREMENT, @@ -140,23 +137,6 @@ def establish_device_mappings(): } ) - CLUSTER_REPORT_CONFIGS.update( - { - SMARTTHINGS_ACCELERATION_CLUSTER: [ - {"attr": "acceleration", "config": REPORT_CONFIG_ASAP}, - {"attr": "x_axis", "config": REPORT_CONFIG_ASAP}, - {"attr": "y_axis", "config": REPORT_CONFIG_ASAP}, - {"attr": "z_axis", "config": REPORT_CONFIG_ASAP}, - ], - SMARTTHINGS_HUMIDITY_CLUSTER: [ - { - "attr": "measured_value", - "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), - } - ], - } - ) - DEVICE_CLASS[zha.PROFILE_ID].update( { SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE: DEVICE_TRACKER, From 2b6674b27f149d5b8b36853fab0a83d0692da10b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Aug 2019 13:40:30 +0200 Subject: [PATCH 072/273] Deprecates usps integration (ADR-0004) (#25743) --- homeassistant/components/usps/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/usps/__init__.py b/homeassistant/components/usps/__init__.py index c33216f5b5b..61da78fa6d7 100644 --- a/homeassistant/components/usps/__init__.py +++ b/homeassistant/components/usps/__init__.py @@ -37,6 +37,12 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Use config values to set up a function enabling status retrieval.""" + _LOGGER.warning( + "The usps integration is deprecated and will be removed " + "in Home Assistant 0.100.0. For more information see ADR-0004:" + "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" + ) + conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) From d3aac130656a92f9d81939bf515d1d41f81a4c91 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Aug 2019 13:40:47 +0200 Subject: [PATCH 073/273] Deprecates sytadin integration (ADR-0004) (#25742) --- homeassistant/components/sytadin/sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/sytadin/sensor.py b/homeassistant/components/sytadin/sensor.py index 9cbb8ee939a..4296f2d5b05 100644 --- a/homeassistant/components/sytadin/sensor.py +++ b/homeassistant/components/sytadin/sensor.py @@ -50,6 +50,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up of the Sytadin Traffic sensor platform.""" + _LOGGER.warning( + "The sytadin integration is deprecated and will be removed " + "in Home Assistant 0.100.0. For more information see ADR-0004:" + "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" + ) + name = config.get(CONF_NAME) sytadin = SytadinData(URL) From b696f9ce5e310af4e6a72df45a6a779cf9d6c94d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Aug 2019 13:41:35 +0200 Subject: [PATCH 074/273] Deprecates ups integration (ADR-0004) (#25746) --- homeassistant/components/ups/sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/ups/sensor.py b/homeassistant/components/ups/sensor.py index 0fff07d442f..cfe35a9a63f 100644 --- a/homeassistant/components/ups/sensor.py +++ b/homeassistant/components/ups/sensor.py @@ -40,6 +40,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the UPS platform.""" import upsmychoice + _LOGGER.warning( + "The ups integration is deprecated and will be removed " + "in Home Assistant 0.100.0. For more information see ADR-0004:" + "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" + ) + try: cookie = hass.config.path(COOKIE) session = upsmychoice.get_session( From 84f464d089caab24d671716ccacd8cffcffa520a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Aug 2019 15:26:43 +0200 Subject: [PATCH 075/273] Deprecates fedex integration (ADR-0004) (#25745) --- homeassistant/components/fedex/sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/fedex/sensor.py b/homeassistant/components/fedex/sensor.py index a18d6a4c651..2f499e52e23 100644 --- a/homeassistant/components/fedex/sensor.py +++ b/homeassistant/components/fedex/sensor.py @@ -44,6 +44,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Fedex platform.""" import fedexdeliverymanager + _LOGGER.warning( + "The fedex integration is deprecated and will be removed " + "in Home Assistant 0.100.0. For more information see ADR-0004:" + "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" + ) + name = config.get(CONF_NAME) update_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) From 07499faa9c75789c488612a7628c752152659247 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Aug 2019 21:41:44 +0200 Subject: [PATCH 076/273] Deprecates srp_energy integration (ADR-0004) (#25754) --- homeassistant/components/srp_energy/sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py index 56d8fcdb4b6..a9873c76afe 100644 --- a/homeassistant/components/srp_energy/sensor.py +++ b/homeassistant/components/srp_energy/sensor.py @@ -43,6 +43,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the SRP energy.""" + _LOGGER.warning( + "The srp_energy integration is deprecated and will be removed " + "in Home Assistant 0.100.0. For more information see ADR-0004:" + "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" + ) + name = config[CONF_NAME] username = config[CONF_USERNAME] password = config[CONF_PASSWORD] From 1739f50b591a2ba464077f0c698571a97b2643e1 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 7 Aug 2019 21:58:11 +0200 Subject: [PATCH 077/273] Update azure-pipelines-wheels.yml for Azure Pipelines --- azure-pipelines-wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index ea43d34f746..77e9cbb95f1 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -18,7 +18,7 @@ schedules: always: true variables: - name: versionWheels - value: '1.0-3.7-alpine3.10' + value: '1.1-3.7-alpine3.10' resources: repositories: - repository: azure From c3455efc1167ad17fe2449dcc841426de07c9b7e Mon Sep 17 00:00:00 2001 From: Santobert Date: Thu, 8 Aug 2019 00:28:22 +0200 Subject: [PATCH 078/273] Updater component is always available and shows on/off depending on whether an update is available or not (#25418) * Updater Component is always available and shows on/off wether an update is available * Use == instead of is to compare strings * Edit log message when local version is newer * One more commit to trigger CI * Add binary sensor * Remove ATTR * Use dispatcher * Use callback instead of async * Make flake happy * Fix callback * discover binary sensor * flake * Fix discovery * prepared tests, TODO * Fix tests * Test release notes * Add one more test * Add another test * Add docstring * Revert "Add another test" This reverts commit 3f896a4e3bbb5a49cf2108ca2cef6e60e2de89f9. * Remove unused file * Update docstrings * mock time * Test renaming entity * Add test_rename_entity * Improve test_rename_entity --- homeassistant/components/updater/__init__.py | 42 +++-- .../components/updater/binary_sensor.py | 81 ++++++++++ tests/components/updater/test_init.py | 145 +++++++++++++++--- 3 files changed, 230 insertions(+), 38 deletions(-) create mode 100644 homeassistant/components/updater/binary_sensor.py diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 9efab69ab26..dd270a0bb75 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -12,22 +12,25 @@ import aiohttp import async_timeout import voluptuous as vol -from homeassistant.const import ATTR_FRIENDLY_NAME, __version__ as current_version +from homeassistant.const import __version__ as current_version from homeassistant.helpers import event from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers import discovery +from homeassistant.helpers.dispatcher import async_dispatcher_send import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) ATTR_RELEASE_NOTES = "release_notes" +ATTR_NEWEST_VERSION = "newest_version" CONF_REPORTING = "reporting" CONF_COMPONENT_REPORTING = "include_used_components" DOMAIN = "updater" -ENTITY_ID = "updater.updater" +DISPATCHER_REMOTE_UPDATE = "updater_remote_update" UPDATER_URL = "https://updater.home-assistant.io/" UPDATER_UUID_FILE = ".uuid" @@ -47,6 +50,16 @@ RESPONSE_SCHEMA = vol.Schema( ) +class Updater: + """Updater class for data exchange.""" + + def __init__(self, update_available: bool, newest_version: str, release_notes: str): + """Initialize attributes.""" + self.update_available = update_available + self.release_notes = release_notes + self.newest_version = newest_version + + def _create_uuid(hass, filename=UPDATER_UUID_FILE): """Create UUID and save it in a file.""" with open(hass.config.path(filename), "w") as fptr: @@ -73,6 +86,10 @@ async def async_setup(hass, config): # This component only makes sense in release versions _LOGGER.info("Running on 'dev', only analytics will be submitted") + hass.async_create_task( + discovery.async_load_platform(hass, "binary_sensor", DOMAIN, {}, config) + ) + config = config.get(DOMAIN, {}) if config.get(CONF_REPORTING): huuid = await hass.async_add_job(_load_uuid, hass) @@ -88,7 +105,7 @@ async def async_setup(hass, config): if result is None: return - newest, releasenotes = result + newest, release_notes = result # Skip on dev if newest is None or "dev" in current_version: @@ -99,18 +116,17 @@ async def async_setup(hass, config): newest = hass.components.hassio.get_homeassistant_version() # Validate version + update_available = False if StrictVersion(newest) > StrictVersion(current_version): - _LOGGER.info("The latest available version is %s", newest) - hass.states.async_set( - ENTITY_ID, - newest, - { - ATTR_FRIENDLY_NAME: "Update Available", - ATTR_RELEASE_NOTES: releasenotes, - }, - ) + _LOGGER.info("The latest available version of Home Assistant is %s", newest) + update_available = True elif StrictVersion(newest) == StrictVersion(current_version): _LOGGER.info("You are on the latest version (%s) of Home Assistant", newest) + elif StrictVersion(newest) < StrictVersion(current_version): + _LOGGER.debug("Local version is newer than the latest version (%s)", newest) + + updater = Updater(update_available, newest, release_notes) + async_dispatcher_send(hass, DISPATCHER_REMOTE_UPDATE, updater) # Update daily, start 1 hour after startup _dt = dt_util.utcnow() + timedelta(hours=1) @@ -151,7 +167,7 @@ async def get_newest_version(hass, huuid, include_components): info_object, ) except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Could not contact Home Assistant Update to check " "for updates") + _LOGGER.error("Could not contact Home Assistant Update to check for updates") return None try: diff --git a/homeassistant/components/updater/binary_sensor.py b/homeassistant/components/updater/binary_sensor.py new file mode 100644 index 00000000000..cae3ae32e3c --- /dev/null +++ b/homeassistant/components/updater/binary_sensor.py @@ -0,0 +1,81 @@ +"""Support for Home Assistant Updater binary sensors.""" + +from homeassistant.core import callback +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import ATTR_NEWEST_VERSION, ATTR_RELEASE_NOTES, DISPATCHER_REMOTE_UPDATE, Updater + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the updater binary sensors.""" + async_add_entities([UpdaterBinary()]) + + +class UpdaterBinary(BinarySensorDevice): + """Representation of an updater binary sensor.""" + + def __init__(self): + """Initialize the binary sensor.""" + self._update_available = None + self._release_notes = None + self._newest_version = None + self._unsub_dispatcher = None + + @property + def name(self) -> str: + """Return the name of the binary sensor, if any.""" + return "Updater" + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return "updater" + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return self._update_available + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._update_available is not None + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state.""" + return False + + @property + def device_state_attributes(self) -> dict: + """Return the optional state attributes.""" + data = super().device_state_attributes + if data is None: + data = {} + if self._release_notes: + data[ATTR_RELEASE_NOTES] = self._release_notes + if self._newest_version: + data[ATTR_NEWEST_VERSION] = self._newest_version + return data + + async def async_added_to_hass(self): + """Register update dispatcher.""" + + @callback + def async_state_update(updater: Updater): + """Update callback.""" + self._newest_version = updater.newest_version + self._release_notes = updater.release_notes + self._update_available = updater.update_available + self.async_schedule_update_ha_state() + + self._unsub_dispatcher = async_dispatcher_connect( + self.hass, DISPATCHER_REMOTE_UPDATE, async_state_update + ) + + async def async_will_remove_from_hass(self): + """Register update dispatcher.""" + if self._unsub_dispatcher is not None: + self._unsub_dispatcher() + self._unsub_dispatcher = None diff --git a/tests/components/updater/test_init.py b/tests/components/updater/test_init.py index 0269f269027..014fb7b6f45 100644 --- a/tests/components/updater/test_init.py +++ b/tests/components/updater/test_init.py @@ -21,6 +21,7 @@ MOCK_DEV_VERSION = "10.0.dev0" MOCK_HUUID = "abcdefg" MOCK_RESPONSE = {"version": "0.15", "release-notes": "https://home-assistant.io"} MOCK_CONFIG = {updater.DOMAIN: {"reporting": True}} +RELEASE_NOTES = "test release notes" @pytest.fixture(autouse=True) @@ -44,56 +45,138 @@ def mock_get_uuid(): yield mock +@pytest.fixture +def mock_utcnow(): + """Fixture to mock utcnow.""" + with patch("homeassistant.components.updater.dt_util.utcnow") as mock: + yield mock + + @asyncio.coroutine -def test_new_version_shows_entity_after_hour( - hass, mock_get_uuid, mock_get_newest_version -): - """Test if new entity is created if new version is available.""" +def test_new_version_shows_entity_startup(hass, mock_get_uuid, mock_get_newest_version): + """Test if binary sensor is unavailable at first.""" mock_get_uuid.return_value = MOCK_HUUID - mock_get_newest_version.return_value = mock_coro((NEW_VERSION, "")) + mock_get_newest_version.return_value = mock_coro((NEW_VERSION, RELEASE_NOTES)) res = yield from async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) assert res, "Updater failed to set up" - with patch("homeassistant.components.updater.current_version", MOCK_VERSION): - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=1)) - yield from hass.async_block_till_done() - - assert hass.states.is_state(updater.ENTITY_ID, NEW_VERSION) + yield from hass.async_block_till_done() + assert hass.states.is_state("binary_sensor.updater", "unavailable") + assert "newest_version" not in hass.states.get("binary_sensor.updater").attributes + assert "release_notes" not in hass.states.get("binary_sensor.updater").attributes @asyncio.coroutine -def test_same_version_not_show_entity(hass, mock_get_uuid, mock_get_newest_version): - """Test if new entity is created if new version is available.""" +def test_rename_entity(hass, mock_get_uuid, mock_get_newest_version): + """Test if renaming the binary sensor works correctly.""" + mock_get_uuid.return_value = MOCK_HUUID + mock_get_newest_version.return_value = mock_coro((NEW_VERSION, RELEASE_NOTES)) + + now = dt_util.utcnow() + later = now + timedelta(hours=1) + mock_utcnow.return_value = now + + res = yield from async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) + assert res, "Updater failed to set up" + + yield from hass.async_block_till_done() + assert hass.states.is_state("binary_sensor.updater", "unavailable") + assert hass.states.get("binary_sensor.new_entity_id") is None + + entity_registry = yield from hass.helpers.entity_registry.async_get_registry() + entity_registry.async_update_entity( + "binary_sensor.updater", new_entity_id="binary_sensor.new_entity_id" + ) + + yield from hass.async_block_till_done() + assert hass.states.is_state("binary_sensor.new_entity_id", "unavailable") + assert hass.states.get("binary_sensor.updater") is None + + with patch("homeassistant.components.updater.current_version", MOCK_VERSION): + async_fire_time_changed(hass, later) + yield from hass.async_block_till_done() + + assert hass.states.is_state("binary_sensor.new_entity_id", "on") + assert hass.states.get("binary_sensor.updater") is None + + +@asyncio.coroutine +def test_new_version_shows_entity_true(hass, mock_get_uuid, mock_get_newest_version): + """Test if sensor is true if new version is available.""" + mock_get_uuid.return_value = MOCK_HUUID + mock_get_newest_version.return_value = mock_coro((NEW_VERSION, RELEASE_NOTES)) + + now = dt_util.utcnow() + later = now + timedelta(hours=1) + mock_utcnow.return_value = now + + res = yield from async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) + assert res, "Updater failed to set up" + + yield from hass.async_block_till_done() + with patch("homeassistant.components.updater.current_version", MOCK_VERSION): + async_fire_time_changed(hass, later) + yield from hass.async_block_till_done() + + assert hass.states.is_state("binary_sensor.updater", "on") + assert ( + hass.states.get("binary_sensor.updater").attributes["newest_version"] + == NEW_VERSION + ) + assert ( + hass.states.get("binary_sensor.updater").attributes["release_notes"] + == RELEASE_NOTES + ) + + +@asyncio.coroutine +def test_same_version_shows_entity_false(hass, mock_get_uuid, mock_get_newest_version): + """Test if sensor is false if no new version is available.""" mock_get_uuid.return_value = MOCK_HUUID mock_get_newest_version.return_value = mock_coro((MOCK_VERSION, "")) + now = dt_util.utcnow() + later = now + timedelta(hours=1) + mock_utcnow.return_value = now + res = yield from async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) assert res, "Updater failed to set up" + yield from hass.async_block_till_done() with patch("homeassistant.components.updater.current_version", MOCK_VERSION): - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=1)) + async_fire_time_changed(hass, later) yield from hass.async_block_till_done() - assert hass.states.get(updater.ENTITY_ID) is None + assert hass.states.is_state("binary_sensor.updater", "off") + assert ( + hass.states.get("binary_sensor.updater").attributes["newest_version"] + == MOCK_VERSION + ) + assert "release_notes" not in hass.states.get("binary_sensor.updater").attributes @asyncio.coroutine def test_disable_reporting(hass, mock_get_uuid, mock_get_newest_version): - """Test if new entity is created if new version is available.""" + """Test we do not gather analytics when disable reporting is active.""" mock_get_uuid.return_value = MOCK_HUUID mock_get_newest_version.return_value = mock_coro((MOCK_VERSION, "")) + now = dt_util.utcnow() + later = now + timedelta(hours=1) + mock_utcnow.return_value = now + res = yield from async_setup_component( hass, updater.DOMAIN, {updater.DOMAIN: {"reporting": False}} ) assert res, "Updater failed to set up" + yield from hass.async_block_till_done() with patch("homeassistant.components.updater.current_version", MOCK_VERSION): - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=1)) + async_fire_time_changed(hass, later) yield from hass.async_block_till_done() - assert hass.states.get(updater.ENTITY_ID) is None + assert hass.states.is_state("binary_sensor.updater", "off") res = yield from updater.get_newest_version(hass, MOCK_HUUID, MOCK_CONFIG) call = mock_get_newest_version.mock_calls[0][1] assert call[0] is hass @@ -114,7 +197,7 @@ def test_get_newest_version_no_analytics_when_no_huuid(hass, aioclient_mock): @asyncio.coroutine def test_get_newest_version_analytics_when_huuid(hass, aioclient_mock): - """Test we do not gather analytics when no huuid is passed in.""" + """Test we gather analytics when huuid is passed in.""" aioclient_mock.post(updater.UPDATER_URL, json=MOCK_RESPONSE) with patch( @@ -127,7 +210,7 @@ def test_get_newest_version_analytics_when_huuid(hass, aioclient_mock): @asyncio.coroutine def test_error_fetching_new_version_timeout(hass): - """Test we do not gather analytics when no huuid is passed in.""" + """Test we handle timeout error while fetching new version.""" with patch( "homeassistant.helpers.system_info.async_get_system_info", Mock(return_value=mock_coro({"fake": "bla"})), @@ -138,7 +221,7 @@ def test_error_fetching_new_version_timeout(hass): @asyncio.coroutine def test_error_fetching_new_version_bad_json(hass, aioclient_mock): - """Test we do not gather analytics when no huuid is passed in.""" + """Test we handle json error while fetching new version.""" aioclient_mock.post(updater.UPDATER_URL, text="not json") with patch( @@ -151,7 +234,7 @@ def test_error_fetching_new_version_bad_json(hass, aioclient_mock): @asyncio.coroutine def test_error_fetching_new_version_invalid_response(hass, aioclient_mock): - """Test we do not gather analytics when no huuid is passed in.""" + """Test we handle response error while fetching new version.""" aioclient_mock.post( updater.UPDATER_URL, json={ @@ -172,17 +255,29 @@ def test_error_fetching_new_version_invalid_response(hass, aioclient_mock): def test_new_version_shows_entity_after_hour_hassio( hass, mock_get_uuid, mock_get_newest_version ): - """Test if new entity is created if new version is available / hass.io.""" + """Test if binary sensor gets updated if new version is available / hass.io.""" mock_get_uuid.return_value = MOCK_HUUID - mock_get_newest_version.return_value = mock_coro((NEW_VERSION, "")) + mock_get_newest_version.return_value = mock_coro((NEW_VERSION, RELEASE_NOTES)) mock_component(hass, "hassio") hass.data["hassio_hass_version"] = "999.0" + now = dt_util.utcnow() + later = now + timedelta(hours=1) + mock_utcnow.return_value = now + res = yield from async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) assert res, "Updater failed to set up" + yield from hass.async_block_till_done() with patch("homeassistant.components.updater.current_version", MOCK_VERSION): - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=1)) + async_fire_time_changed(hass, later) yield from hass.async_block_till_done() - assert hass.states.is_state(updater.ENTITY_ID, "999.0") + assert hass.states.is_state("binary_sensor.updater", "on") + assert ( + hass.states.get("binary_sensor.updater").attributes["newest_version"] == "999.0" + ) + assert ( + hass.states.get("binary_sensor.updater").attributes["release_notes"] + == RELEASE_NOTES + ) From d1b9ebc7b2a3aa582643d06548f286cd1886f049 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 8 Aug 2019 00:35:50 +0200 Subject: [PATCH 079/273] Integration requirement check refactor (#25626) * Factor out code getting requirements for integration * Have process requirements raise an exception * One more lint fix * Blackify * Catch new exception * Let RequirementsNotFound be a HomeAssistantError * Correct another test * Split catching of exceptions and avoid complete log --- homeassistant/auth/mfa_modules/__init__.py | 7 +-- homeassistant/auth/providers/__init__.py | 7 +-- homeassistant/config.py | 49 +++++------------ homeassistant/helpers/check_config.py | 62 +++++++--------------- homeassistant/requirements.py | 47 ++++++++++++---- homeassistant/setup.py | 8 +-- tests/helpers/test_check_config.py | 6 +-- tests/scripts/test_check_config.py | 7 +-- tests/test_requirements.py | 43 ++++++++++++--- 9 files changed, 115 insertions(+), 121 deletions(-) diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index fa9b1f50224..5481b8fe08b 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -164,14 +164,9 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.Modul processed = hass.data[DATA_REQS] = set() # https://github.com/python/mypy/issues/1424 - req_success = await requirements.async_process_requirements( + await requirements.async_process_requirements( hass, module_path, module.REQUIREMENTS # type: ignore ) - if not req_success: - raise HomeAssistantError( - "Unable to process requirements of mfa module {}".format(module_name) - ) - processed.add(module_name) return module diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index c720cf0df64..c35af2e0b96 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -165,15 +165,10 @@ async def load_auth_provider_module( # https://github.com/python/mypy/issues/1424 reqs = module.REQUIREMENTS # type: ignore - req_success = await requirements.async_process_requirements( + await requirements.async_process_requirements( hass, "auth provider {}".format(provider), reqs ) - if not req_success: - raise HomeAssistantError( - "Unable to process requirements of auth provider {}".format(provider) - ) - processed.add(provider) return module diff --git a/homeassistant/config.py b/homeassistant/config.py index 3ba59fd80ec..4d3d4dd841f 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -53,8 +53,11 @@ from homeassistant.const import ( ) from homeassistant.core import DOMAIN as CONF_CORE, SOURCE_YAML, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import Integration, async_get_integration, IntegrationNotFound -from homeassistant.requirements import async_process_requirements +from homeassistant.loader import Integration, IntegrationNotFound +from homeassistant.requirements import ( + async_get_integration_with_requirements, + RequirementsNotFound, +) from homeassistant.util.yaml import load_yaml, SECRET_YAML from homeassistant.util.package import is_docker_env import homeassistant.helpers.config_validation as cv @@ -658,27 +661,12 @@ async def merge_packages_config( domain = comp_name.split(" ")[0] try: - integration = await async_get_integration(hass, domain) - except IntegrationNotFound: - _log_pkg_error(pack_name, comp_name, config, "does not exist") - continue - - if ( - not hass.config.skip_pip - and integration.requirements - and not await async_process_requirements( - hass, integration.domain, integration.requirements + integration = await async_get_integration_with_requirements( + hass, domain ) - ): - _log_pkg_error( - pack_name, comp_name, config, "unable to install all requirements" - ) - continue - - try: component = integration.get_component() - except ImportError: - _log_pkg_error(pack_name, comp_name, config, "unable to import") + except (IntegrationNotFound, RequirementsNotFound, ImportError) as ex: + _log_pkg_error(pack_name, comp_name, config, str(ex)) continue if hasattr(component, "PLATFORM_SCHEMA"): @@ -775,26 +763,15 @@ async def async_process_component_config( continue try: - p_integration = await async_get_integration(hass, p_name) - except IntegrationNotFound: - continue - - if ( - not hass.config.skip_pip - and p_integration.requirements - and not await async_process_requirements( - hass, p_integration.domain, p_integration.requirements - ) - ): - _LOGGER.error( - "Unable to install all requirements for %s.%s", domain, p_name - ) + p_integration = await async_get_integration_with_requirements(hass, p_name) + except (RequirementsNotFound, IntegrationNotFound) as ex: + _LOGGER.error("Platform error: %s - %s", domain, ex) continue try: platform = p_integration.get_platform(domain) except ImportError: - _LOGGER.exception("Failed to get platform %s.%s", domain, p_name) + _LOGGER.exception("Platform error: %s", domain) continue # Validate platform specific schema diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index a2b4a8580f9..331599e9b0f 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -5,7 +5,7 @@ from typing import List import attr import voluptuous as vol -from homeassistant import loader, requirements +from homeassistant import loader from homeassistant.core import HomeAssistant from homeassistant.config import ( CONF_CORE, @@ -18,6 +18,10 @@ from homeassistant.config import ( extract_domain_configs, config_per_platform, ) +from homeassistant.requirements import ( + async_get_integration_with_requirements, + RequirementsNotFound, +) import homeassistant.util.yaml.loader as yaml_loader from homeassistant.exceptions import HomeAssistantError @@ -101,29 +105,15 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig # Process and validate config for domain in components: try: - integration = await loader.async_get_integration(hass, domain) - except loader.IntegrationNotFound: - result.add_error("Integration 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 - ) - ): - result.add_error( - "Unable to install all requirements: {}".format( - ", ".join(integration.requirements) - ) - ) + integration = await async_get_integration_with_requirements(hass, domain) + except (RequirementsNotFound, loader.IntegrationNotFound) as ex: + result.add_error("Component error: {} - {}".format(domain, ex)) continue try: component = integration.get_component() - except ImportError: - result.add_error("Component not found: {}".format(domain)) + except ImportError as ex: + result.add_error("Component error: {} - {}".format(domain, ex)) continue config_schema = getattr(component, "CONFIG_SCHEMA", None) @@ -161,32 +151,16 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig continue try: - p_integration = await loader.async_get_integration(hass, p_name) - except loader.IntegrationNotFound: - result.add_error( - "Integration {} not found when trying to verify its {} " - "platform.".format(p_name, domain) + p_integration = await async_get_integration_with_requirements( + hass, p_name ) - continue - - if ( - not hass.config.skip_pip - and p_integration.requirements - and not await requirements.async_process_requirements( - hass, p_integration.domain, p_integration.requirements - ) - ): - result.add_error( - "Unable to install all requirements: {}".format( - ", ".join(integration.requirements) - ) - ) - continue - - try: platform = p_integration.get_platform(domain) - except ImportError: - result.add_error("Platform not found: {}.{}".format(domain, p_name)) + except ( + loader.IntegrationNotFound, + RequirementsNotFound, + ImportError, + ) as ex: + result.add_error("Platform error {}.{} - {}".format(domain, p_name, ex)) continue # Validate platform specific schema diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 09cdee2c35a..bdc7798e4f8 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -5,8 +5,10 @@ import logging import os from typing import Any, Dict, List, Optional +from homeassistant.exceptions import HomeAssistantError import homeassistant.util.package as pkg_util from homeassistant.core import HomeAssistant +from homeassistant.loader import async_get_integration, Integration DATA_PIP_LOCK = "pip_lock" DATA_PKG_CACHE = "pkg_cache" @@ -15,12 +17,44 @@ PROGRESS_FILE = ".pip_progress" _LOGGER = logging.getLogger(__name__) +class RequirementsNotFound(HomeAssistantError): + """Raised when a component is not found.""" + + def __init__(self, domain: str, requirements: List) -> None: + """Initialize a component not found error.""" + super().__init__( + "Requirements for {} not found: {}.".format(domain, requirements) + ) + self.domain = domain + self.requirements = requirements + + +async def async_get_integration_with_requirements( + hass: HomeAssistant, domain: str +) -> Integration: + """Get an integration with installed requirements. + + This can raise IntegrationNotFound if manifest or integration + is invalid, RequirementNotFound if there was some type of + failure to install requirements. + """ + integration = await async_get_integration(hass, domain) + + if hass.config.skip_pip or not integration.requirements: + return integration + + await async_process_requirements(hass, integration.domain, integration.requirements) + + return integration + + async def async_process_requirements( hass: HomeAssistant, name: str, requirements: List[str] -) -> bool: +) -> None: """Install the requirements for a component or platform. - This method is a coroutine. + This method is a coroutine. It will raise RequirementsNotFound + if an requirement can't be satisfied. """ pip_lock = hass.data.get(DATA_PIP_LOCK) if pip_lock is None: @@ -36,14 +70,7 @@ async def async_process_requirements( ret = await hass.async_add_executor_job(_install, hass, req, kwargs) if not ret: - _LOGGER.error( - "Not initializing %s because could not install " "requirement %s", - name, - req, - ) - return False - - return True + raise RequirementsNotFound(name, [req]) def _install(hass: HomeAssistant, req: str, kwargs: Dict) -> bool: diff --git a/homeassistant/setup.py b/homeassistant/setup.py index dd29bf3ab09..78bcb2e6505 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -283,14 +283,10 @@ async def async_process_deps_reqs( ): raise HomeAssistantError("Could not set up all dependencies.") - if ( - not hass.config.skip_pip - and integration.requirements - and not await requirements.async_process_requirements( + if not hass.config.skip_pip and integration.requirements: + await requirements.async_process_requirements( hass, integration.domain, integration.requirements ) - ): - raise HomeAssistantError("Could not install all requirements.") processed.add(integration.domain) diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 2a60b9ee2a4..9e5ea15293a 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -75,7 +75,7 @@ async def test_component_platform_not_found(hass, loop): assert res.keys() == {"homeassistant"} assert res.errors[0] == CheckConfigError( - "Integration not found: beer", None, None + "Component error: beer - Integration beer not found.", None, None ) # Only 1 error expected @@ -95,9 +95,7 @@ async def test_component_platform_not_found_2(hass, loop): assert res["light"] == [] assert res.errors[0] == CheckConfigError( - "Integration beer not found when trying to verify its " "light platform.", - None, - None, + "Platform error light.beer - Integration beer not found.", None, None ) # Only 1 error expected diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index a07b812bc96..bd4f37bd135 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -62,7 +62,9 @@ def test_component_platform_not_found(isfile_patch, loop): res = check_config.check(get_test_config_dir()) assert res["components"].keys() == {"homeassistant"} assert res["except"] == { - check_config.ERROR_STR: ["Integration not found: beer"] + check_config.ERROR_STR: [ + "Component error: beer - Integration beer not found." + ] } assert res["secret_cache"] == {} assert res["secrets"] == {} @@ -75,8 +77,7 @@ def test_component_platform_not_found(isfile_patch, loop): assert res["components"]["light"] == [] assert res["except"] == { check_config.ERROR_STR: [ - "Integration beer not found when trying to verify its " - "light platform." + "Platform error light.beer - Integration beer not found." ] } assert res["secret_cache"] == {} diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 486374e3909..b5574fe96fd 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -2,13 +2,16 @@ import os from pathlib import Path from unittest.mock import patch, call +from pytest import raises from homeassistant import setup from homeassistant.requirements import ( CONSTRAINT_FILE, + async_get_integration_with_requirements, async_process_requirements, PROGRESS_FILE, _install, + RequirementsNotFound, ) from tests.common import get_test_home_assistant, MockModule, mock_integration @@ -74,22 +77,50 @@ async def test_install_existing_package(hass): with patch( "homeassistant.util.package.install_package", return_value=True ) as mock_inst: - assert await async_process_requirements( - hass, "test_component", ["hello==1.0.0"] - ) + await async_process_requirements(hass, "test_component", ["hello==1.0.0"]) assert len(mock_inst.mock_calls) == 1 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"] - ) + await async_process_requirements(hass, "test_component", ["hello==1.0.0"]) assert len(mock_inst.mock_calls) == 0 +async def test_install_missing_package(hass): + """Test an install attempt on an existing package.""" + with patch( + "homeassistant.util.package.install_package", return_value=False + ) as mock_inst: + with raises(RequirementsNotFound): + await async_process_requirements(hass, "test_component", ["hello==1.0.0"]) + + assert len(mock_inst.mock_calls) == 1 + + +async def test_get_integration_with_requirements(hass): + """Check getting an integration with loaded requirements.""" + hass.config.skip_pip = False + mock_integration(hass, MockModule("test_component", requirements=["hello==1.0.0"])) + + with patch( + "homeassistant.util.package.is_installed", return_value=False + ) as mock_is_installed, patch( + "homeassistant.util.package.install_package", return_value=True + ) as mock_inst: + + integration = await async_get_integration_with_requirements( + hass, "test_component" + ) + assert integration + assert integration.domain == "test_component" + + assert len(mock_is_installed.mock_calls) == 1 + assert len(mock_inst.mock_calls) == 1 + + async def test_install_with_wheels_index(hass): """Test an install attempt with wheels index URL.""" hass.config.skip_pip = False From 0fa1e3ac927c44c962b63d4918b18f029f9f54df Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 8 Aug 2019 11:32:52 +0200 Subject: [PATCH 080/273] Update azure-pipelines-release.yml --- azure-pipelines-release.yml | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 768e9627e4c..13a031fda15 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -11,29 +11,23 @@ variables: - group: docker - group: github - group: twine +resources: + repositories: + - repository: azure + type: github + name: 'home-assistant/ci-azure' + endpoint: 'home-assistant' stages: - stage: 'Validate' jobs: - - job: 'VersionValidate' + - template: templates/azp-job-version.yaml@azure + - job: 'Permission' 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' - script: | sudo apt-get install -y --no-install-recommends \ jq curl From 4bcef25486b740cbfa686634133849a6591f23ff Mon Sep 17 00:00:00 2001 From: Robert Dunmire III Date: Thu, 8 Aug 2019 07:58:13 -0400 Subject: [PATCH 081/273] Add Mikrotik hub and rework device tracker (#25664) * Add const.py for Mikrotik hub * Add Mikrotik hub component * Rework device tracker to use hub * Fix validation errors * Fix line spacing * Bump librouteros version to 2.3.0 * Bump librouteros version to 2.3.0 * Used black code formatter * Fix validation errors * Fix errors * Fix errors * Renamed MikrotikAPI to MikrotikClient * Fix method * Fix device_tracker and rename ssl to use_ssl * Moved device tracker functions into device tracker * Fix missing constants * Fix device tracker host_name * Fix errors * Fix device tracker typo * Adding device tracker attributes * Change attributes order * Change attributes order * Add one more attribute * Reformat black * Exclude Mikrotik modules * Remove async calls * Remove unused import * Adding scan interval to device tracker * Fix errors and update code * Fix error * Fix missing period * Update device tracker to use setup_scanner * Fix hass.data HOSTS * Fix errors * Fix errors * Fixes and updates * Fixing and reworking * Fixes * Fix constant INFO * get_hostname fix and return value --- .coveragerc | 2 +- homeassistant/components/mikrotik/__init__.py | 195 +++++++++ homeassistant/components/mikrotik/const.py | 49 +++ .../components/mikrotik/device_tracker.py | 371 ++++++++---------- .../components/mikrotik/manifest.json | 2 +- requirements_all.txt | 2 +- 6 files changed, 402 insertions(+), 219 deletions(-) create mode 100644 homeassistant/components/mikrotik/const.py diff --git a/.coveragerc b/.coveragerc index 75b97e8f5e1..24844630f4a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -374,7 +374,7 @@ omit = homeassistant/components/metoffice/weather.py homeassistant/components/microsoft/tts.py homeassistant/components/miflora/sensor.py - homeassistant/components/mikrotik/device_tracker.py + homeassistant/components/mikrotik/* homeassistant/components/mill/climate.py homeassistant/components/mitemp_bt/sensor.py homeassistant/components/mjpeg/camera.py diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index 0fe5a1c70b1..cbab4812322 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -1 +1,196 @@ """The mikrotik component.""" +import logging +import ssl + +import voluptuous as vol +import librouteros +from librouteros.login import login_plain, login_token + +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + CONF_PORT, + CONF_SSL, + CONF_METHOD, +) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.discovery import load_platform +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER +from .const import ( + DOMAIN, + HOSTS, + MTK_LOGIN_PLAIN, + MTK_LOGIN_TOKEN, + DEFAULT_ENCODING, + IDENTITY, + CONF_TRACK_DEVICES, + CONF_ENCODING, + CONF_ARP_PING, + CONF_LOGIN_METHOD, + MIKROTIK_SERVICES, +) + +_LOGGER = logging.getLogger(__name__) + +MTK_DEFAULT_API_PORT = "8728" +MTK_DEFAULT_API_SSL_PORT = "8729" + +MIKROTIK_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_METHOD): cv.string, + vol.Optional(CONF_LOGIN_METHOD): vol.Any(MTK_LOGIN_PLAIN, MTK_LOGIN_TOKEN), + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, + vol.Optional(CONF_TRACK_DEVICES, default=True): cv.boolean, + vol.Optional(CONF_ARP_PING, default=False): cv.boolean, + } + ) +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.All(cv.ensure_list, [MIKROTIK_SCHEMA])}, extra=vol.ALLOW_EXTRA +) + + +def setup(hass, config): + """Set up the Mikrotik component.""" + hass.data[DOMAIN] = {HOSTS: {}} + + for device in config[DOMAIN]: + host = device[CONF_HOST] + use_ssl = device.get(CONF_SSL) + user = device.get(CONF_USERNAME) + password = device.get(CONF_PASSWORD, "") + login = device.get(CONF_LOGIN_METHOD) + encoding = device.get(CONF_ENCODING) + track_devices = device.get(CONF_TRACK_DEVICES) + + if CONF_PORT in device: + port = device.get(CONF_PORT) + else: + if use_ssl: + port = MTK_DEFAULT_API_SSL_PORT + else: + port = MTK_DEFAULT_API_PORT + + if login == MTK_LOGIN_PLAIN: + login_method = (login_plain,) + elif login == MTK_LOGIN_TOKEN: + login_method = (login_token,) + else: + login_method = (login_plain, login_token) + + try: + api = MikrotikClient( + host, use_ssl, port, user, password, login_method, encoding + ) + api.connect_to_device() + hass.data[DOMAIN][HOSTS][host] = {"config": device, "api": api} + except ( + librouteros.exceptions.TrapError, + librouteros.exceptions.MultiTrapError, + librouteros.exceptions.ConnectionError, + ) as api_error: + _LOGGER.error("Mikrotik %s error %s", host, api_error) + continue + + if track_devices: + hass.data[DOMAIN][HOSTS][host][DEVICE_TRACKER] = True + load_platform(hass, DEVICE_TRACKER, DOMAIN, None, config) + + if not hass.data[DOMAIN][HOSTS]: + return False + return True + + +class MikrotikClient: + """Handle all communication with the Mikrotik API.""" + + def __init__(self, host, use_ssl, port, user, password, login_method, encoding): + """Initialize the Mikrotik Client.""" + self._host = host + self._use_ssl = use_ssl + self._port = port + self._user = user + self._password = password + self._login_method = login_method + self._encoding = encoding + self.hostname = None + self._client = None + self._connected = False + + def connect_to_device(self): + """Connect to Mikrotik device.""" + self._connected = False + _LOGGER.debug("[%s] Connecting to Mikrotik device", self._host) + + kwargs = { + "encoding": self._encoding, + "login_methods": self._login_method, + "port": self._port, + } + + if self._use_ssl: + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + kwargs["ssl_wrapper"] = ssl_context.wrap_socket + + try: + self._client = librouteros.connect( + self._host, self._user, self._password, **kwargs + ) + self._connected = True + except ( + librouteros.exceptions.TrapError, + librouteros.exceptions.MultiTrapError, + librouteros.exceptions.ConnectionError, + ) as api_error: + _LOGGER.error("Mikrotik %s: %s", self._host, api_error) + self._client = None + return False + + self.hostname = self.get_hostname() + _LOGGER.info("Mikrotik Connected to %s (%s)", self.hostname, self._host) + return self._connected + + def get_hostname(self): + """Return device host name.""" + data = self.command(MIKROTIK_SERVICES[IDENTITY]) + return data[0]["name"] if data else None + + def connected(self): + """Return connected boolean.""" + return self._connected + + def command(self, cmd, params=None): + """Retrieve data from Mikrotik API.""" + if not self._connected or not self._client: + return None + try: + if params: + response = self._client(cmd=cmd, **params) + else: + response = self._client(cmd=cmd) + except (librouteros.exceptions.ConnectionError,) as api_error: + _LOGGER.error("Mikrotik %s connection error %s", self._host, api_error) + self.connect_to_device() + return None + except ( + librouteros.exceptions.TrapError, + librouteros.exceptions.MultiTrapError, + ) as api_error: + _LOGGER.error( + "Mikrotik %s failed to retrieve data. cmd=[%s] Error: %s", + self._host, + cmd, + api_error, + ) + return None + return response if response else None diff --git a/homeassistant/components/mikrotik/const.py b/homeassistant/components/mikrotik/const.py new file mode 100644 index 00000000000..4f511d6b418 --- /dev/null +++ b/homeassistant/components/mikrotik/const.py @@ -0,0 +1,49 @@ +"""Constants used in the Mikrotik components.""" + +DOMAIN = "mikrotik" +MIKROTIK = DOMAIN +HOSTS = "hosts" +MTK_LOGIN_PLAIN = "plain" +MTK_LOGIN_TOKEN = "token" + +CONF_ARP_PING = "arp_ping" +CONF_TRACK_DEVICES = "track_devices" +CONF_LOGIN_METHOD = "login_method" +CONF_ENCODING = "encoding" +DEFAULT_ENCODING = "utf-8" + +INFO = "info" +IDENTITY = "identity" +ARP = "arp" +DHCP = "dhcp" +WIRELESS = "wireless" +CAPSMAN = "capsman" + +MIKROTIK_SERVICES = { + INFO: "/system/routerboard/getall", + IDENTITY: "/system/identity/getall", + ARP: "/ip/arp/getall", + DHCP: "/ip/dhcp-server/lease/getall", + WIRELESS: "/interface/wireless/registration-table/getall", + CAPSMAN: "/caps-man/registration-table/getall", +} + +ATTR_DEVICE_TRACKER = [ + "comment", + "mac-address", + "ssid", + "interface", + "host-name", + "last-seen", + "rx-signal", + "signal-strength", + "tx-ccq", + "signal-to-noise", + "wmm-enabled", + "authentication-type", + "encryption", + "tx-rate-set", + "rx-rate", + "tx-rate", + "uptime", +] diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 3260ac1ab2c..47d3fab28ad 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -1,251 +1,190 @@ """Support for Mikrotik routers as device tracker.""" import logging -import ssl - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( - DOMAIN, - PLATFORM_SCHEMA, + DOMAIN as DEVICE_TRACKER, DeviceScanner, ) -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, - CONF_PORT, - CONF_SSL, - CONF_METHOD, +from homeassistant.util import slugify +from homeassistant.const import CONF_METHOD +from .const import ( + HOSTS, + MIKROTIK, + CONF_ARP_PING, + MIKROTIK_SERVICES, + CAPSMAN, + WIRELESS, + DHCP, + ARP, + ATTR_DEVICE_TRACKER, ) _LOGGER = logging.getLogger(__name__) -MTK_DEFAULT_API_PORT = "8728" -MTK_DEFAULT_API_SSL_PORT = "8729" - -CONF_LOGIN_METHOD = "login_method" -MTK_LOGIN_PLAIN = "plain" -MTK_LOGIN_TOKEN = "token" - -CONF_ENCODING = "encoding" -DEFAULT_ENCODING = "utf-8" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_METHOD): cv.string, - vol.Optional(CONF_LOGIN_METHOD): vol.Any(MTK_LOGIN_PLAIN, MTK_LOGIN_TOKEN), - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, - } -) - def get_scanner(hass, config): - """Validate the configuration and return MTikScanner.""" - scanner = MikrotikScanner(config[DOMAIN]) + """Validate the configuration and return MikrotikScanner.""" + for host in hass.data[MIKROTIK][HOSTS]: + if DEVICE_TRACKER not in hass.data[MIKROTIK][HOSTS][host]: + continue + hass.data[MIKROTIK][HOSTS][host].pop(DEVICE_TRACKER, None) + api = hass.data[MIKROTIK][HOSTS][host]["api"] + config = hass.data[MIKROTIK][HOSTS][host]["config"] + hostname = api.get_hostname() + scanner = MikrotikScanner(api, host, hostname, config) return scanner if scanner.success_init else None class MikrotikScanner(DeviceScanner): - """This class queries a Mikrotik router.""" + """This class queries a Mikrotik device.""" - def __init__(self, config): + def __init__(self, api, host, hostname, config): """Initialize the scanner.""" - self.last_results = {} - - self.host = config[CONF_HOST] - self.ssl = config[CONF_SSL] - try: - self.port = config[CONF_PORT] - except KeyError: - if self.ssl: - self.port = MTK_DEFAULT_API_SSL_PORT - else: - self.port = MTK_DEFAULT_API_PORT - self.username = config[CONF_USERNAME] - self.password = config[CONF_PASSWORD] - self.login_method = config.get(CONF_LOGIN_METHOD) + self.api = api + self.config = config + self.host = host + self.hostname = hostname self.method = config.get(CONF_METHOD) - self.encoding = config[CONF_ENCODING] + self.arp_ping = config.get(CONF_ARP_PING) + self.dhcp = None + self.devices_arp = {} + self.devices_dhcp = {} + self.device_tracker = None + self.success_init = self.api.connected() - self.connected = False - self.success_init = False - self.client = None - self.wireless_exist = None - self.success_init = self.connect_to_device() + def get_extra_attributes(self, device): + """ + Get extra attributes of a device. - if self.success_init: - _LOGGER.info("Start polling Mikrotik (%s) router...", self.host) - self._update_info() - else: - _LOGGER.error("Connection to Mikrotik (%s) failed", self.host) + Some known extra attributes that may be returned in the device tuple + include MAC address (mac), network device (dev), IP address + (ip), reachable status (reachable), associated router + (host), hostname if known (hostname) among others. + """ + return self.device_tracker.get(device) or {} - def connect_to_device(self): - """Connect to Mikrotik method.""" - import librouteros - from librouteros.login import login_plain, login_token - - if self.login_method == MTK_LOGIN_PLAIN: - login_method = (login_plain,) - elif self.login_method == MTK_LOGIN_TOKEN: - login_method = (login_token,) - else: - login_method = (login_plain, login_token) - - try: - kwargs = { - "port": self.port, - "encoding": self.encoding, - "login_methods": login_method, - } - - if self.ssl: - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - kwargs["ssl_wrapper"] = ssl_context.wrap_socket - self.client = librouteros.connect( - self.host, self.username, self.password, **kwargs - ) - - try: - routerboard_info = self.client(cmd="/system/routerboard/getall") - except ( - librouteros.exceptions.TrapError, - librouteros.exceptions.MultiTrapError, - librouteros.exceptions.ConnectionError, - ): - routerboard_info = None - raise - - if routerboard_info: - _LOGGER.info( - "Connected to Mikrotik %s with IP %s", - routerboard_info[0].get("model", "Router"), - self.host, - ) - - self.connected = True - - try: - self.capsman_exist = self.client(cmd="/caps-man/interface/getall") - except ( - librouteros.exceptions.TrapError, - librouteros.exceptions.MultiTrapError, - librouteros.exceptions.ConnectionError, - ): - self.capsman_exist = False - - if not self.capsman_exist: - _LOGGER.info( - "Mikrotik %s: Not a CAPSman controller. Trying " - "local interfaces", - self.host, - ) - - try: - self.wireless_exist = self.client(cmd="/interface/wireless/getall") - except ( - librouteros.exceptions.TrapError, - librouteros.exceptions.MultiTrapError, - librouteros.exceptions.ConnectionError, - ): - self.wireless_exist = False - - if ( - not self.wireless_exist - and not self.capsman_exist - or self.method == "ip" - ): - _LOGGER.info( - "Mikrotik %s: Wireless adapters not found. Try to " - "use DHCP lease table as presence tracker source. " - "Please decrease lease time as much as possible", - self.host, - ) - if self.method: - _LOGGER.info( - "Mikrotik %s: Manually selected polling method %s", - self.host, - self.method, - ) - - except ( - librouteros.exceptions.TrapError, - librouteros.exceptions.MultiTrapError, - librouteros.exceptions.ConnectionError, - ) as api_error: - _LOGGER.error("Connection error: %s", api_error) - return self.connected + def get_device_name(self, device): + """Get name for a device.""" + host = self.device_tracker.get(device, {}) + return host.get("host_name") def scan_devices(self): """Scan for new devices and return a list with found device MACs.""" - import librouteros + self.update_device_tracker() + return list(self.device_tracker) - try: - self._update_info() - except ( - librouteros.exceptions.TrapError, - librouteros.exceptions.MultiTrapError, - librouteros.exceptions.ConnectionError, - ) as api_error: - _LOGGER.error("Connection error: %s", api_error) - self.connect_to_device() - return [device for device in self.last_results] - - def get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - return self.last_results.get(device) - - def _update_info(self): - """Retrieve latest information from the Mikrotik box.""" + def get_method(self): + """Determine the device tracker polling method.""" if self.method: - devices_tracker = self.method + _LOGGER.debug( + "Mikrotik %s: Manually selected polling method %s", + self.host, + self.method, + ) + return self.method + + capsman = self.api.command(MIKROTIK_SERVICES[CAPSMAN]) + if not capsman: + _LOGGER.debug( + "Mikrotik %s: Not a CAPsMAN controller. " + "Trying local wireless interfaces", + (self.host), + ) else: - if self.capsman_exist: - devices_tracker = "capsman" - elif self.wireless_exist: - devices_tracker = "wireless" - else: - devices_tracker = "ip" + return CAPSMAN - _LOGGER.debug( - "Loading %s devices from Mikrotik (%s) ...", devices_tracker, self.host - ) + wireless = self.api.command(MIKROTIK_SERVICES[WIRELESS]) + if not wireless: + _LOGGER.info( + "Mikrotik %s: Wireless adapters not found. Try to " + "use DHCP lease table as presence tracker source. " + "Please decrease lease time as much as possible", + self.host, + ) + return DHCP - device_names = self.client(cmd="/ip/dhcp-server/lease/getall") - if devices_tracker == "capsman": - devices = self.client(cmd="/caps-man/registration-table/getall") - elif devices_tracker == "wireless": - devices = self.client(cmd="/interface/wireless/registration-table/getall") - else: - devices = device_names + return WIRELESS - if device_names is None and devices is None: - return False + def update_device_tracker(self): + """Update device_tracker from Mikrotik API.""" + self.device_tracker = {} + if not self.method: + self.method = self.get_method() - mac_names = { - device.get("mac-address"): device.get("host-name") - for device in device_names - if device.get("mac-address") + data = self.api.command(MIKROTIK_SERVICES[self.method]) + if data is None: + return + + if self.method != DHCP: + dhcp = self.api.command(MIKROTIK_SERVICES[DHCP]) + if dhcp is not None: + self.devices_dhcp = load_mac(dhcp) + + arp = self.api.command(MIKROTIK_SERVICES[ARP]) + self.devices_arp = load_mac(arp) + + for device in data: + mac = device.get("mac-address") + if self.method == DHCP: + if "active-address" not in device: + continue + + if self.arp_ping and self.devices_arp: + if mac not in self.devices_arp: + continue + interface = self.devices_arp[mac]["interface"] + if not self.do_arp_ping(mac, interface): + continue + + attrs = {} + if mac in self.devices_dhcp and "host-name" in self.devices_dhcp[mac]: + hostname = self.devices_dhcp[mac].get("host-name") + if hostname: + attrs["host_name"] = hostname + + if self.devices_arp and mac in self.devices_arp: + attrs["ip_address"] = self.devices_arp[mac].get("address") + + for attr in ATTR_DEVICE_TRACKER: + if attr in device and device[attr] is not None: + attrs[slugify(attr)] = device[attr] + + attrs["scanner_type"] = self.method + attrs["scanner_host"] = self.host + attrs["scanner_hostname"] = self.hostname + self.device_tracker[mac] = attrs + + def do_arp_ping(self, mac, interface): + """Attempt to arp ping MAC address via interface.""" + params = { + "arp-ping": "yes", + "interval": "100ms", + "count": 3, + "interface": interface, + "address": mac, } + cmd = "/ping" + data = self.api.command(cmd, params) + if data is not None: + status = 0 + for result in data: + if "status" in result: + _LOGGER.debug( + "Mikrotik %s arp_ping error: %s", self.host, result["status"] + ) + status += 1 + if status == len(data): + return None + return data - if devices_tracker in ("wireless", "capsman"): - self.last_results = { - device.get("mac-address"): mac_names.get(device.get("mac-address")) - for device in devices - } - else: - self.last_results = { - device.get("mac-address"): mac_names.get(device.get("mac-address")) - for device in device_names - if device.get("active-address") - } - return True +def load_mac(devices=None): + """Load dictionary using MAC address as key.""" + if not devices: + return None + mac_devices = {} + for device in devices: + if "mac-address" in device: + mac = device.pop("mac-address") + mac_devices[mac] = device + return mac_devices diff --git a/homeassistant/components/mikrotik/manifest.json b/homeassistant/components/mikrotik/manifest.json index caa9733f241..92869856545 100644 --- a/homeassistant/components/mikrotik/manifest.json +++ b/homeassistant/components/mikrotik/manifest.json @@ -3,7 +3,7 @@ "name": "Mikrotik", "documentation": "https://www.home-assistant.io/components/mikrotik", "requirements": [ - "librouteros==2.2.0" + "librouteros==2.3.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 21bc1127c5a..aa3602fb92c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -714,7 +714,7 @@ libpurecool==0.5.0 libpyfoscam==1.0 # homeassistant.components.mikrotik -librouteros==2.2.0 +librouteros==2.3.0 # homeassistant.components.soundtouch libsoundtouch==0.7.2 From 8f2f77083761cd58d186dd7ee6a23e8b4be3cf08 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 8 Aug 2019 14:43:53 -0500 Subject: [PATCH 082/273] Don't track unstable attributes (#25787) --- homeassistant/components/unifi/device_tracker.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 89d3fce515e..d9f90de7888 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -41,11 +41,7 @@ LOGGER = logging.getLogger(__name__) DEVICE_ATTRIBUTES = [ "_is_guest_by_uap", - "ap_mac", "authorized", - "bssid", - "ccq", - "channel", "essid", "hostname", "ip", @@ -54,14 +50,11 @@ DEVICE_ATTRIBUTES = [ "is_wired", "mac", "name", - "noise", "noted", "oui", "qos_policy_applied", "radio", "radio_proto", - "rssi", - "signal", "site_id", "vlan", ] From 4349b8640e8bb480076698b7b20206ac9d99598e Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Thu, 8 Aug 2019 21:59:33 +0100 Subject: [PATCH 083/273] Update Cisco Mobility Express module version (#25770) * Update manifest.json * Update requirements_all.txt --- homeassistant/components/cisco_mobility_express/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cisco_mobility_express/manifest.json b/homeassistant/components/cisco_mobility_express/manifest.json index 1d80076793d..abdd2400311 100644 --- a/homeassistant/components/cisco_mobility_express/manifest.json +++ b/homeassistant/components/cisco_mobility_express/manifest.json @@ -3,7 +3,7 @@ "name": "Cisco mobility express", "documentation": "https://www.home-assistant.io/components/cisco_mobility_express", "requirements": [ - "ciscomobilityexpress==0.3.1" + "ciscomobilityexpress==0.3.3" ], "dependencies": [], "codeowners": ["@fbradyirl"] diff --git a/requirements_all.txt b/requirements_all.txt index aa3602fb92c..905f27efa14 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -321,7 +321,7 @@ buienradar==1.0.1 caldav==0.6.1 # homeassistant.components.cisco_mobility_express -ciscomobilityexpress==0.3.1 +ciscomobilityexpress==0.3.3 # homeassistant.components.ciscospark ciscosparkapi==0.4.2 From e47ed0e182099cc796ec8244342870cdf5d1e2d7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 9 Aug 2019 16:47:08 +0200 Subject: [PATCH 084/273] Deprecates linksys_ap integration (ADR-0004) (#25804) --- homeassistant/components/linksys_ap/device_tracker.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/linksys_ap/device_tracker.py b/homeassistant/components/linksys_ap/device_tracker.py index df24a409b98..d40de718f90 100644 --- a/homeassistant/components/linksys_ap/device_tracker.py +++ b/homeassistant/components/linksys_ap/device_tracker.py @@ -30,6 +30,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_scanner(hass, config): """Validate the configuration and return a Linksys AP scanner.""" + _LOGGER.warning( + "The linksys_ap integration is deprecated and will be removed " + "in Home Assistant 0.100.0. For more information see ADR-0004:" + "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" + ) + try: return LinksysAPDeviceScanner(config[DOMAIN]) except ConnectionError: From ecddeb2dd1e4484eaaebc50a1adb0f494ebbd8a3 Mon Sep 17 00:00:00 2001 From: miroslawkrol <53930034+miroslawkrol@users.noreply.github.com> Date: Fri, 9 Aug 2019 17:21:18 +0200 Subject: [PATCH 085/273] Fix Broadlink MP1 unavailable error (#25806) --- homeassistant/components/broadlink/switch.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 2a7255f5a61..4d3fa644f47 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -337,6 +337,10 @@ class BroadlinkMP1Slot(BroadlinkRMSwitch): """Trigger update for all switches on the parent device.""" self._parent_device.update() self._state = self._parent_device.get_outlet_status(self._slot) + if self._state is None: + self._is_available = False + else: + self._is_available = True class BroadlinkMP1Switch: From 8b6ddc22a5f49136933a2e5eb2fb7994ed04d7ec Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Fri, 9 Aug 2019 20:31:58 +0300 Subject: [PATCH 086/273] Fix deconz allow_clip_sensor and allow_deconz_groups options (#25811) --- homeassistant/components/deconz/gateway.py | 4 ++-- tests/components/deconz/test_binary_sensor.py | 10 +++++++--- tests/components/deconz/test_climate.py | 10 +++++++--- tests/components/deconz/test_light.py | 10 +++++++--- tests/components/deconz/test_sensor.py | 10 +++++++--- 5 files changed, 30 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 8eca227f0cd..0ed3ffd2a56 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -63,12 +63,12 @@ class DeconzGateway: @property def allow_clip_sensor(self) -> bool: """Allow loading clip sensor from gateway.""" - return self.config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) + return self.config_entry.options.get(CONF_ALLOW_CLIP_SENSOR, True) @property def allow_deconz_groups(self) -> bool: """Allow loading deCONZ groups from gateway.""" - return self.config_entry.data.get(CONF_ALLOW_DECONZ_GROUPS, True) + return self.config_entry.options.get(CONF_ALLOW_DECONZ_GROUPS, True) async def async_update_device_registry(self): """Update device registry.""" diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 4978a6f75d0..9eb408ba4f1 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -31,14 +31,17 @@ SENSOR = { ENTRY_CONFIG = { - deconz.const.CONF_ALLOW_CLIP_SENSOR: True, - deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, deconz.config_flow.CONF_API_KEY: "ABCDEF", deconz.config_flow.CONF_BRIDGEID: "0123456789", deconz.config_flow.CONF_HOST: "1.2.3.4", deconz.config_flow.CONF_PORT: 80, } +ENTRY_OPTIONS = { + deconz.const.CONF_ALLOW_CLIP_SENSOR: True, + deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, +} + async def setup_gateway(hass, data, allow_clip_sensor=True): """Load the deCONZ binary sensor platform.""" @@ -47,7 +50,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): loop = Mock() session = Mock() - ENTRY_CONFIG[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor + ENTRY_OPTIONS[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor config_entry = config_entries.ConfigEntry( 1, @@ -56,6 +59,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + ENTRY_OPTIONS, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 2f2bcbed255..264c3b8761f 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -39,14 +39,17 @@ SENSOR = { } ENTRY_CONFIG = { - deconz.const.CONF_ALLOW_CLIP_SENSOR: True, - deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, deconz.config_flow.CONF_API_KEY: "ABCDEF", deconz.config_flow.CONF_BRIDGEID: "0123456789", deconz.config_flow.CONF_HOST: "1.2.3.4", deconz.config_flow.CONF_PORT: 80, } +ENTRY_OPTIONS = { + deconz.const.CONF_ALLOW_CLIP_SENSOR: True, + deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, +} + async def setup_gateway(hass, data, allow_clip_sensor=True): """Load the deCONZ sensor platform.""" @@ -59,7 +62,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): session = Mock(put=asynctest.CoroutineMock(return_value=response)) - ENTRY_CONFIG[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor + ENTRY_OPTIONS[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor config_entry = config_entries.ConfigEntry( 1, @@ -68,6 +71,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + ENTRY_OPTIONS, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(hass.loop, session, **config_entry.data) diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 2d5ba57b6de..77e983e34b4 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -62,14 +62,17 @@ SWITCH = { ENTRY_CONFIG = { - deconz.const.CONF_ALLOW_CLIP_SENSOR: True, - deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, deconz.config_flow.CONF_API_KEY: "ABCDEF", deconz.config_flow.CONF_BRIDGEID: "0123456789", deconz.config_flow.CONF_HOST: "1.2.3.4", deconz.config_flow.CONF_PORT: 80, } +ENTRY_OPTIONS = { + deconz.const.CONF_ALLOW_CLIP_SENSOR: True, + deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, +} + async def setup_gateway(hass, data, allow_deconz_groups=True): """Load the deCONZ light platform.""" @@ -78,7 +81,7 @@ async def setup_gateway(hass, data, allow_deconz_groups=True): loop = Mock() session = Mock() - ENTRY_CONFIG[deconz.const.CONF_ALLOW_DECONZ_GROUPS] = allow_deconz_groups + ENTRY_OPTIONS[deconz.const.CONF_ALLOW_DECONZ_GROUPS] = allow_deconz_groups config_entry = config_entries.ConfigEntry( 1, @@ -87,6 +90,7 @@ async def setup_gateway(hass, data, allow_deconz_groups=True): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + ENTRY_OPTIONS, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index d881a87a6e6..9c03f3e9a90 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -75,14 +75,17 @@ SENSOR = { ENTRY_CONFIG = { - deconz.const.CONF_ALLOW_CLIP_SENSOR: True, - deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, deconz.config_flow.CONF_API_KEY: "ABCDEF", deconz.config_flow.CONF_BRIDGEID: "0123456789", deconz.config_flow.CONF_HOST: "1.2.3.4", deconz.config_flow.CONF_PORT: 80, } +ENTRY_OPTIONS = { + deconz.const.CONF_ALLOW_CLIP_SENSOR: True, + deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, +} + async def setup_gateway(hass, data, allow_clip_sensor=True): """Load the deCONZ sensor platform.""" @@ -91,7 +94,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): loop = Mock() session = Mock() - ENTRY_CONFIG[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor + ENTRY_OPTIONS[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor config_entry = config_entries.ConfigEntry( 1, @@ -100,6 +103,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + ENTRY_OPTIONS, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) From a0494e44ebe02b80d71265e76154bf503862468a Mon Sep 17 00:00:00 2001 From: Dustin Essington Date: Fri, 9 Aug 2019 10:54:33 -0700 Subject: [PATCH 087/273] Update HIBP sensor to use API v3 and API Key (#25699) * Update HIBP sensor to use API v3 and API Key * ran black code formatter * fixed stray , that was invalid in multiple json formatters --- .../components/haveibeenpwned/manifest.json | 12 ++++----- .../components/haveibeenpwned/sensor.py | 27 +++++++++---------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/haveibeenpwned/manifest.json b/homeassistant/components/haveibeenpwned/manifest.json index f0b0561e170..40572f82ea8 100644 --- a/homeassistant/components/haveibeenpwned/manifest.json +++ b/homeassistant/components/haveibeenpwned/manifest.json @@ -1,8 +1,8 @@ { - "domain": "haveibeenpwned", - "name": "Haveibeenpwned", - "documentation": "https://www.home-assistant.io/components/haveibeenpwned", - "requirements": [], - "dependencies": [], - "codeowners": [] + "domain": "haveibeenpwned", + "name": "Haveibeenpwned", + "documentation": "https://www.home-assistant.io/components/haveibeenpwned", + "requirements": [], + "dependencies": [], + "codeowners": [] } diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index d78756b9543..ec43d9444a2 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -7,7 +7,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_EMAIL, ATTR_ATTRIBUTION +from homeassistant.const import CONF_EMAIL, CONF_API_KEY, ATTR_ATTRIBUTION import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_point_in_time @@ -25,17 +25,21 @@ HA_USER_AGENT = "Home Assistant HaveIBeenPwned Sensor Component" MIN_TIME_BETWEEN_FORCED_UPDATES = timedelta(seconds=5) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) -URL = "https://haveibeenpwned.com/api/v2/breachedaccount/" +URL = "https://haveibeenpwned.com/api/v3/breachedaccount/" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_EMAIL): vol.All(cv.ensure_list, [cv.string])} + { + vol.Required(CONF_EMAIL): vol.All(cv.ensure_list, [cv.string]), + vol.Required(CONF_API_KEY): cv.string, + } ) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the HaveIBeenPwned sensor.""" emails = config.get(CONF_EMAIL) - data = HaveIBeenPwnedData(emails) + api_key = config[CONF_API_KEY] + data = HaveIBeenPwnedData(emails, api_key) devices = [] for email in emails: @@ -125,13 +129,14 @@ class HaveIBeenPwnedSensor(Entity): class HaveIBeenPwnedData: """Class for handling the data retrieval.""" - def __init__(self, emails): + def __init__(self, emails, api_key): """Initialize the data object.""" self._email_count = len(emails) self._current_index = 0 self.data = {} self._email = emails[0] self._emails = emails + self._api_key = api_key def set_next_email(self): """Set the next email to be looked up.""" @@ -146,16 +151,10 @@ class HaveIBeenPwnedData: def update(self, **kwargs): """Get the latest data for current email from REST service.""" try: - url = "{}{}".format(URL, self._email) - + url = "{}{}?truncateResponse=false".format(URL, self._email) + header = {USER_AGENT: HA_USER_AGENT, "hibp-api-key": self._api_key} _LOGGER.debug("Checking for breaches for email: %s", self._email) - - req = requests.get( - url, - headers={USER_AGENT: HA_USER_AGENT}, - allow_redirects=True, - timeout=5, - ) + req = requests.get(url, headers=header, allow_redirects=True, timeout=5) except requests.exceptions.RequestException: _LOGGER.error("Failed fetching data for %s", self._email) From 5b516fc0cdb24a44d8477adb851de15172f0125e Mon Sep 17 00:00:00 2001 From: Tomi Lehto Date: Fri, 9 Aug 2019 21:16:47 +0300 Subject: [PATCH 088/273] Add arcus trigonometry functions to templates (#25510) --- homeassistant/helpers/template.py | 43 ++++++++++++++ tests/helpers/test_template.py | 95 +++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index c591fe218f1..ca320cb1c33 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -714,6 +714,41 @@ def tangent(value): return value +def arc_sine(value): + """Filter to get arc sine of the value.""" + try: + return math.asin(float(value)) + except (ValueError, TypeError): + return value + + +def arc_cosine(value): + """Filter to get arc cosine of the value.""" + try: + return math.acos(float(value)) + except (ValueError, TypeError): + return value + + +def arc_tangent(value): + """Filter to get arc tangent of the value.""" + try: + return math.atan(float(value)) + except (ValueError, TypeError): + return value + + +def arc_tangent2(*args): + """Filter to calculate four quadrant arc tangent of y / x.""" + try: + if len(args) == 1 and isinstance(args[0], (list, tuple)): + args = args[0] + + return math.atan2(float(args[0]), float(args[1])) + except (ValueError, TypeError): + return args + + def square_root(value): """Filter to get square root of the value.""" try: @@ -872,6 +907,10 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["sin"] = sine self.filters["cos"] = cosine self.filters["tan"] = tangent + self.filters["asin"] = arc_sine + self.filters["acos"] = arc_cosine + self.filters["atan"] = arc_tangent + self.filters["atan2"] = arc_tangent2 self.filters["sqrt"] = square_root self.filters["as_timestamp"] = forgiving_as_timestamp self.filters["timestamp_custom"] = timestamp_custom @@ -899,6 +938,10 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["pi"] = math.pi self.globals["tau"] = math.pi * 2 self.globals["e"] = math.e + self.globals["asin"] = arc_sine + self.globals["acos"] = arc_cosine + self.globals["atan"] = arc_tangent + self.globals["atan2"] = arc_tangent2 self.globals["float"] = forgiving_float self.globals["now"] = dt_util.now self.globals["utcnow"] = dt_util.utcnow diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index d1beb4eb47b..cc1f7707df6 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -349,6 +349,101 @@ def test_sqrt(hass): ) +def test_arc_sine(hass): + """Test arcus sine.""" + tests = [ + (-2.0, "-2.0"), # value error + (-1.0, "-1.571"), + (-0.5, "-0.524"), + (0.0, "0.0"), + (0.5, "0.524"), + (1.0, "1.571"), + (2.0, "2.0"), # value error + ('"error"', "error"), + ] + + for value, expected in tests: + assert ( + template.Template("{{ %s | asin | round(3) }}" % value, hass).async_render() + == expected + ) + + +def test_arc_cos(hass): + """Test arcus cosine.""" + tests = [ + (-2.0, "-2.0"), # value error + (-1.0, "3.142"), + (-0.5, "2.094"), + (0.0, "1.571"), + (0.5, "1.047"), + (1.0, "0.0"), + (2.0, "2.0"), # value error + ('"error"', "error"), + ] + + for value, expected in tests: + assert ( + template.Template("{{ %s | acos | round(3) }}" % value, hass).async_render() + == expected + ) + + +def test_arc_tan(hass): + """Test arcus tangent.""" + tests = [ + (-10.0, "-1.471"), + (-2.0, "-1.107"), + (-1.0, "-0.785"), + (-0.5, "-0.464"), + (0.0, "0.0"), + (0.5, "0.464"), + (1.0, "0.785"), + (2.0, "1.107"), + (10.0, "1.471"), + ('"error"', "error"), + ] + + for value, expected in tests: + assert ( + template.Template("{{ %s | atan | round(3) }}" % value, hass).async_render() + == expected + ) + + +def test_arc_tan2(hass): + """Test two parameter version of arcus tangent.""" + tests = [ + (-10.0, -10.0, "-2.356"), + (-10.0, 0.0, "-1.571"), + (-10.0, 10.0, "-0.785"), + (0.0, -10.0, "3.142"), + (0.0, 0.0, "0.0"), + (0.0, 10.0, "0.0"), + (10.0, -10.0, "2.356"), + (10.0, 0.0, "1.571"), + (10.0, 10.0, "0.785"), + (-4.0, 3.0, "-0.927"), + (-1.0, 2.0, "-0.464"), + (2.0, 1.0, "1.107"), + ('"duck"', '"goose"', "('duck', 'goose')"), + ] + + for y, x, expected in tests: + assert ( + template.Template( + "{{ (%s, %s) | atan2 | round(3) }}" % (y, x), hass + ).async_render() + == expected + ) + assert ( + template.Template( + "{{ atan2(%s, %s) | round(3) }}" % (y, x), hass + ).async_render() + == expected + ) + + def test_strptime(hass): """Test the parse timestamp method.""" tests = [ From 6909235d8a30833757beaba118c11aeba13339e2 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 9 Aug 2019 13:41:50 -0500 Subject: [PATCH 089/273] Fix brightness type (#25818) --- homeassistant/components/smartthings/light.py | 4 +++- tests/components/smartthings/test_light.py | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smartthings/light.py b/homeassistant/components/smartthings/light.py index 9ec4634ab36..4bc3f487790 100644 --- a/homeassistant/components/smartthings/light.py +++ b/homeassistant/components/smartthings/light.py @@ -133,7 +133,9 @@ class SmartThingsLight(SmartThingsEntity, Light): """Update entity attributes when the device status has changed.""" # Brightness and transition if self._supported_features & SUPPORT_BRIGHTNESS: - self._brightness = convert_scale(self._device.status.level, 100, 255) + self._brightness = int( + convert_scale(self._device.status.level, 100, 255, 0) + ) # Color Temperature if self._supported_features & SUPPORT_COLOR_TEMP: self._color_temp = color_util.color_temperature_kelvin_to_mired( diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index b0f7268217c..e9004031e7d 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -84,6 +84,7 @@ async def test_entity_state(hass, light_devices): state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION ) + assert isinstance(state.attributes[ATTR_BRIGHTNESS], int) assert state.attributes[ATTR_BRIGHTNESS] == 255 # Color Dimmer 1 @@ -103,6 +104,7 @@ async def test_entity_state(hass, light_devices): ) assert state.attributes[ATTR_BRIGHTNESS] == 255 assert state.attributes[ATTR_HS_COLOR] == (273.6, 55.0) + assert isinstance(state.attributes[ATTR_COLOR_TEMP], int) assert state.attributes[ATTR_COLOR_TEMP] == 222 @@ -191,7 +193,7 @@ async def test_turn_on_with_brightness(hass, light_devices): assert state is not None assert state.state == "on" # round-trip rounding error (expected) - assert state.attributes[ATTR_BRIGHTNESS] == 73.95 + assert state.attributes[ATTR_BRIGHTNESS] == 74 async def test_turn_on_with_minimal_brightness(hass, light_devices): @@ -216,7 +218,7 @@ async def test_turn_on_with_minimal_brightness(hass, light_devices): assert state is not None assert state.state == "on" # round-trip rounding error (expected) - assert state.attributes[ATTR_BRIGHTNESS] == 2.55 + assert state.attributes[ATTR_BRIGHTNESS] == 3 async def test_turn_on_with_color(hass, light_devices): From a66814c77213aa224402634a6513e8bd19b7022f Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 9 Aug 2019 21:08:35 +0200 Subject: [PATCH 090/273] Add script to install locale (#25791) --- Dockerfile | 1 + virtualization/Docker/Dockerfile.dev | 1 + virtualization/Docker/scripts/locales | 12 ++++++++++++ virtualization/Docker/setup_docker_prereqs | 5 +++++ 4 files changed, 19 insertions(+) create mode 100755 virtualization/Docker/scripts/locales diff --git a/Dockerfile b/Dockerfile index 09c16707541..a9e73699558 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ LABEL maintainer="Paulus Schoutsen " #ENV INSTALL_SSOCR no #ENV INSTALL_DLIB no #ENV INSTALL_IPERF3 no +#ENV INSTALL_LOCALES no VOLUME /config diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev index 2191d8ad920..260a29cb3d0 100644 --- a/virtualization/Docker/Dockerfile.dev +++ b/virtualization/Docker/Dockerfile.dev @@ -14,6 +14,7 @@ LABEL maintainer="Paulus Schoutsen " #ENV INSTALL_SSOCR no #ENV INSTALL_DLIB no #ENV INSTALL_IPERF3 no +#ENV INSTALL_LOCALES no VOLUME /config diff --git a/virtualization/Docker/scripts/locales b/virtualization/Docker/scripts/locales new file mode 100755 index 00000000000..cbbe0341575 --- /dev/null +++ b/virtualization/Docker/scripts/locales @@ -0,0 +1,12 @@ +#!/bin/bash +# Sets up locales. + +# Stop on errors +set -e + +apt-get update +apt-get install -y --no-install-recommends locales + +# Set the locale +sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen +locale-gen diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index 9f3fc81d045..62ac73d366e 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -9,6 +9,7 @@ INSTALL_OPENALPR="${INSTALL_OPENALPR:-yes}" INSTALL_LIBCEC="${INSTALL_LIBCEC:-yes}" INSTALL_SSOCR="${INSTALL_SSOCR:-yes}" INSTALL_DLIB="${INSTALL_DLIB:-yes}" +INSTALL_LOCALES="${INSTALL_LOCALES:-yes}" # Required debian packages for running hass or components PACKAGES=( @@ -70,6 +71,10 @@ if [ "$INSTALL_DLIB" == "yes" ]; then pip3 install --no-cache-dir "dlib>=19.5" fi +if [ "$INSTALL_LOCALES" == "yes" ]; then + virtualization/Docker/scripts/locales +fi + # Remove packages apt-get remove -y --purge ${PACKAGES_DEV[@]} apt-get -y --purge autoremove From f58106c7b7eba6cbff5fb2d63c47ab60738f1fdc Mon Sep 17 00:00:00 2001 From: Brandon Davidson Date: Fri, 9 Aug 2019 13:20:26 -0700 Subject: [PATCH 091/273] Update pyvera to 0.3.3 (#25820) Fixes #24987 --- homeassistant/components/vera/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index 5fddce7efe7..07ae7ab3d36 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -3,7 +3,7 @@ "name": "Vera", "documentation": "https://www.home-assistant.io/components/vera", "requirements": [ - "pyvera==0.3.2" + "pyvera==0.3.3" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 905f27efa14..46d72fc8d19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1570,7 +1570,7 @@ pyuptimerobot==0.0.5 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.3.2 +pyvera==0.3.3 # homeassistant.components.vesync pyvesync==1.1.0 From 60dfa38717d19f739a865191d35b3c22e941ab35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Lov=C3=A9n?= Date: Fri, 9 Aug 2019 22:21:08 +0200 Subject: [PATCH 092/273] Add error handling to !include command in yaml (#25801) * Catch errors if !include file is not found * Address review comments * Add line number to error message --- homeassistant/util/yaml/loader.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 3cbf0481673..eda3f12905d 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -116,7 +116,10 @@ def _include_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE: """ fname = os.path.join(os.path.dirname(loader.name), node.value) - return _add_reference(load_yaml(fname), loader, node) + try: + return _add_reference(load_yaml(fname), loader, node) + except FileNotFoundError: + raise HomeAssistantError(f"{node.start_mark}: Unable to read file {fname}.") def _is_file_valid(name: str) -> bool: From dc5c1783dc76f01e7ab91eeea1eafd7286153e01 Mon Sep 17 00:00:00 2001 From: escoand Date: Sat, 10 Aug 2019 00:14:03 +0200 Subject: [PATCH 093/273] Webhook for Traccar (#24762) * add initial traccar webhook support * remove unused import * add tests but disabled atm * remove translations * add timestamp parameter * use post for tests * rename config_flow * format using black * format tests using black * Use str instead of float * fix most comments * check id * add two device test * reformat * fix failuers * Update tests/components/traccar/test_init.py Co-Authored-By: Martin Hjelmare * Update tests/components/traccar/test_init.py Co-Authored-By: Martin Hjelmare * Update tests/components/traccar/test_init.py Co-Authored-By: Martin Hjelmare * Update tests/components/traccar/test_init.py Co-Authored-By: Martin Hjelmare * black --- homeassistant/components/traccar/__init__.py | 111 +++++++- .../components/traccar/config_flow.py | 10 + homeassistant/components/traccar/const.py | 12 +- .../components/traccar/device_tracker.py | 177 ++++++++++++- .../components/traccar/manifest.json | 7 +- homeassistant/generated/config_flows.py | 1 + tests/components/traccar/__init__.py | 1 + tests/components/traccar/test_init.py | 243 ++++++++++++++++++ 8 files changed, 556 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/traccar/config_flow.py create mode 100644 tests/components/traccar/__init__.py create mode 100644 tests/components/traccar/test_init.py diff --git a/homeassistant/components/traccar/__init__.py b/homeassistant/components/traccar/__init__.py index 03805760c53..8e3f90fb66f 100644 --- a/homeassistant/components/traccar/__init__.py +++ b/homeassistant/components/traccar/__init__.py @@ -1 +1,110 @@ -"""The traccar component.""" +"""Support for Traccar.""" +import logging + +import voluptuous as vol +from aiohttp import web + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, HTTP_OK, CONF_WEBHOOK_ID +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 ( + ATTR_ACCURACY, + ATTR_ALTITUDE, + ATTR_BATTERY, + ATTR_BEARING, + ATTR_ID, + ATTR_LATITUDE, + ATTR_LONGITUDE, + ATTR_SPEED, + ATTR_TIMESTAMP, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +TRACKER_UPDATE = "{}_tracker_update".format(DOMAIN) + + +DEFAULT_ACCURACY = 200 +DEFAULT_BATTERY = -1 + + +def _id(value: str) -> str: + """Coerce id by removing '-'.""" + return value.replace("-", "") + + +WEBHOOK_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ID): vol.All(cv.string, _id), + vol.Required(ATTR_LATITUDE): cv.latitude, + vol.Required(ATTR_LONGITUDE): cv.longitude, + vol.Optional(ATTR_ACCURACY, default=DEFAULT_ACCURACY): vol.Coerce(float), + vol.Optional(ATTR_ALTITUDE): vol.Coerce(float), + vol.Optional(ATTR_BATTERY, default=DEFAULT_BATTERY): vol.Coerce(float), + vol.Optional(ATTR_BEARING): vol.Coerce(float), + vol.Optional(ATTR_SPEED): vol.Coerce(float), + vol.Optional(ATTR_TIMESTAMP): vol.Coerce(int), + } +) + + +async def async_setup(hass, hass_config): + """Set up the Traccar component.""" + hass.data[DOMAIN] = {"devices": set(), "unsub_device_tracker": {}} + return True + + +async def handle_webhook(hass, webhook_id, request): + """Handle incoming webhook with Traccar request.""" + try: + data = WEBHOOK_SCHEMA(dict(request.query)) + except vol.MultipleInvalid as error: + return web.Response(text=error.error_message, status=HTTP_UNPROCESSABLE_ENTITY) + + attrs = { + ATTR_ALTITUDE: data.get(ATTR_ALTITUDE), + ATTR_BEARING: data.get(ATTR_BEARING), + ATTR_SPEED: data.get(ATTR_SPEED), + } + + device = data[ATTR_ID] + + async_dispatcher_send( + hass, + TRACKER_UPDATE, + device, + data[ATTR_LATITUDE], + data[ATTR_LONGITUDE], + data[ATTR_BATTERY], + data[ATTR_ACCURACY], + attrs, + ) + + return web.Response(text="Setting location for {}".format(device), status=HTTP_OK) + + +async def async_setup_entry(hass, entry): + """Configure based on config entry.""" + hass.components.webhook.async_register( + DOMAIN, "Traccar", entry.data[CONF_WEBHOOK_ID], handle_webhook + ) + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, DEVICE_TRACKER) + ) + return True + + +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 + + +# pylint: disable=invalid-name +async_remove_entry = config_entry_flow.webhook_async_remove_entry diff --git a/homeassistant/components/traccar/config_flow.py b/homeassistant/components/traccar/config_flow.py new file mode 100644 index 00000000000..cc3f1f23727 --- /dev/null +++ b/homeassistant/components/traccar/config_flow.py @@ -0,0 +1,10 @@ +"""Config flow for Traccar.""" +from homeassistant.helpers import config_entry_flow +from .const import DOMAIN + + +config_entry_flow.register_webhook_flow( + DOMAIN, + "Traccar Webhook", + {"docs_url": "https://www.home-assistant.io/components/traccar/"}, +) diff --git a/homeassistant/components/traccar/const.py b/homeassistant/components/traccar/const.py index 58f7168cf43..56c0ab5ba1d 100644 --- a/homeassistant/components/traccar/const.py +++ b/homeassistant/components/traccar/const.py @@ -1,16 +1,26 @@ """Constants for Traccar integration.""" +DOMAIN = "traccar" + CONF_MAX_ACCURACY = "max_accuracy" CONF_SKIP_ACCURACY_ON = "skip_accuracy_filter_on" +ATTR_ACCURACY = "accuracy" ATTR_ADDRESS = "address" +ATTR_ALTITUDE = "altitude" +ATTR_BATTERY = "batt" +ATTR_BEARING = "bearing" ATTR_CATEGORY = "category" ATTR_GEOFENCE = "geofence" +ATTR_ID = "id" +ATTR_LATITUDE = "lat" +ATTR_LONGITUDE = "lon" ATTR_MOTION = "motion" ATTR_SPEED = "speed" +ATTR_STATUS = "status" +ATTR_TIMESTAMP = "timestamp" ATTR_TRACKER = "tracker" ATTR_TRACCAR_ID = "traccar_id" -ATTR_STATUS = "status" EVENT_DEVICE_MOVING = "device_moving" EVENT_COMMAND_RESULT = "command_result" diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index bc1eebf05da..c7fdda013b0 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol -from homeassistant.components.device_tracker import PLATFORM_SCHEMA +from homeassistant.core import callback from homeassistant.const import ( CONF_HOST, CONF_PORT, @@ -16,19 +16,33 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_EVENT, ) +from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.helpers import device_registry +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import HomeAssistantType import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import slugify + +from . import DOMAIN, TRACKER_UPDATE from .const import ( + ATTR_ACCURACY, ATTR_ADDRESS, + ATTR_ALTITUDE, + ATTR_BATTERY, + ATTR_BEARING, ATTR_CATEGORY, ATTR_GEOFENCE, + ATTR_LATITUDE, + ATTR_LONGITUDE, ATTR_MOTION, ATTR_SPEED, + ATTR_STATUS, ATTR_TRACKER, ATTR_TRACCAR_ID, - ATTR_STATUS, EVENT_DEVICE_MOVING, EVENT_COMMAND_RESULT, EVENT_DEVICE_FUEL_DROP, @@ -101,6 +115,45 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +async def async_setup_entry(hass: HomeAssistantType, entry, async_add_entities): + """Configure a dispatcher connection based on a config entry.""" + + @callback + def _receive_data(device, latitude, longitude, battery, accuracy, attrs): + """Receive set location.""" + if device in hass.data[DOMAIN]["devices"]: + return + + hass.data[DOMAIN]["devices"].add(device) + + async_add_entities( + [TraccarEntity(device, latitude, longitude, battery, accuracy, attrs)] + ) + + hass.data[DOMAIN]["unsub_device_tracker"][ + 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] == DOMAIN + } + if not dev_ids: + return + + entities = [] + for dev_id in dev_ids: + hass.data[DOMAIN]["devices"].add(dev_id) + entity = TraccarEntity(dev_id, None, None, None, None, None) + entities.append(entity) + + async_add_entities(entities) + + async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Validate the configuration and return a Traccar scanner.""" from pytraccar.api import API @@ -273,3 +326,123 @@ class TraccarScanner: "attributes": event["attributes"], }, ) + + +class TraccarEntity(TrackerEntity, RestoreEntity): + """Represent a tracked device.""" + + def __init__(self, device, latitude, longitude, battery, accuracy, attributes): + """Set up Geofency entity.""" + self._accuracy = accuracy + self._attributes = attributes + self._name = device + self._battery = battery + self._latitude = latitude + self._longitude = longitude + self._unsub_dispatcher = None + self._unique_id = device + + @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._latitude + + @property + def longitude(self): + """Return longitude value of the device.""" + return self._longitude + + @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 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": {(DOMAIN, self._unique_id)}} + + @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.""" + await super().async_added_to_hass() + self._unsub_dispatcher = async_dispatcher_connect( + self.hass, TRACKER_UPDATE, self._async_receive_data + ) + + # don't restore if we got created with data + if self._latitude is not None or self._longitude is not None: + return + + state = await self.async_get_last_state() + if state is None: + self._latitude = None + self._longitude = None + self._accuracy = None + self._attributes = { + ATTR_ALTITUDE: None, + ATTR_BEARING: None, + ATTR_SPEED: None, + } + self._battery = None + return + + attr = state.attributes + self._latitude = attr.get(ATTR_LATITUDE) + self._longitude = attr.get(ATTR_LONGITUDE) + self._accuracy = attr.get(ATTR_ACCURACY) + self._attributes = { + ATTR_ALTITUDE: attr.get(ATTR_ALTITUDE), + ATTR_BEARING: attr.get(ATTR_BEARING), + ATTR_SPEED: attr.get(ATTR_SPEED), + } + self._battery = attr.get(ATTR_BATTERY) + + async def async_will_remove_from_hass(self): + """Clean up after entity before removal.""" + await super().async_will_remove_from_hass() + self._unsub_dispatcher() + + @callback + def _async_receive_data( + self, device, latitude, longitude, battery, accuracy, attributes + ): + """Mark the device as seen.""" + if device != self.name: + return + + self._latitude = latitude + self._longitude = longitude + self._battery = battery + self._accuracy = accuracy + self._attributes.update(attributes) + self.async_write_ha_state() diff --git a/homeassistant/components/traccar/manifest.json b/homeassistant/components/traccar/manifest.json index 15b78d0ec7b..7d3e2f22d65 100644 --- a/homeassistant/components/traccar/manifest.json +++ b/homeassistant/components/traccar/manifest.json @@ -1,13 +1,16 @@ { "domain": "traccar", "name": "Traccar", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/traccar", "requirements": [ "pytraccar==0.9.0", "stringcase==1.2.0" ], - "dependencies": [], + "dependencies": [ + "webhook" + ], "codeowners": [ "@ludeeus" ] -} \ No newline at end of file +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 827b946f776..497686d0a2e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -51,6 +51,7 @@ FLOWS = [ "tellduslive", "toon", "tplink", + "traccar", "tradfri", "twentemilieu", "twilio", diff --git a/tests/components/traccar/__init__.py b/tests/components/traccar/__init__.py new file mode 100644 index 00000000000..48c7818452f --- /dev/null +++ b/tests/components/traccar/__init__.py @@ -0,0 +1 @@ +"""Tests for the Traccar component.""" diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py new file mode 100644 index 00000000000..5a2dabcf6c2 --- /dev/null +++ b/tests/components/traccar/test_init.py @@ -0,0 +1,243 @@ +"""The tests the for Traccar device tracker platform.""" +from unittest.mock import Mock, patch + +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components import traccar, zone +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN +from homeassistant.components.traccar import DOMAIN, TRACKER_UPDATE +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 + +HOME_LATITUDE = 37.239622 +HOME_LONGITUDE = -115.815811 + + +@pytest.fixture(autouse=True) +def mock_dev_track(mock_device_tracker_conf): + """Mock device tracker config loading.""" + pass + + +@pytest.fixture(name="client") +async def traccar_client(loop, hass, aiohttp_client): + """Mock client for Traccar (unauthenticated).""" + assert await async_setup_component(hass, "persistent_notification", {}) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + await hass.async_block_till_done() + + with patch("homeassistant.components.device_tracker.legacy.update_config"): + return await aiohttp_client(hass.http.app) + + +@pytest.fixture(autouse=True) +async def setup_zones(loop, hass): + """Set up Zone config in HA.""" + assert await async_setup_component( + hass, + zone.DOMAIN, + { + "zone": { + "name": "Home", + "latitude": HOME_LATITUDE, + "longitude": HOME_LONGITUDE, + "radius": 100, + } + }, + ) + await hass.async_block_till_done() + + +@pytest.fixture(name="webhook_id") +async def webhook_id_fixture(hass, client): + """Initialize the Traccar component and get the webhook_id.""" + hass.config.api = Mock(base_url="http://example.com") + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + return result["result"].data["webhook_id"] + + +async def test_missing_data(hass, client, webhook_id): + """Test missing data.""" + url = "/api/webhook/{}".format(webhook_id) + data = {"lat": "1.0", "lon": "1.1", "id": "123"} + + # No data + req = await client.post(url) + await hass.async_block_till_done() + assert req.status == HTTP_UNPROCESSABLE_ENTITY + + # No latitude + copy = data.copy() + del copy["lat"] + req = await client.post(url, params=copy) + await hass.async_block_till_done() + assert req.status == HTTP_UNPROCESSABLE_ENTITY + + # No device + copy = data.copy() + del copy["id"] + req = await client.post(url, params=copy) + await hass.async_block_till_done() + assert req.status == HTTP_UNPROCESSABLE_ENTITY + + +async def test_enter_and_exit(hass, client, webhook_id): + """Test when there is a known zone.""" + url = "/api/webhook/{}".format(webhook_id) + data = {"lat": str(HOME_LATITUDE), "lon": str(HOME_LONGITUDE), "id": "123"} + + # Enter the Home + req = await client.post(url, params=data) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get( + "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"]) + ).state + assert STATE_HOME == state_name + + # Enter Home again + req = await client.post(url, params=data) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get( + "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"]) + ).state + assert STATE_HOME == state_name + + data["lon"] = 0 + data["lat"] = 0 + + # Enter Somewhere else + req = await client.post(url, params=data) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get( + "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"]) + ).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, client, webhook_id): + """Test when additional attributes are present.""" + url = "/api/webhook/{}".format(webhook_id) + data = { + "timestamp": 123456789, + "lat": "1.0", + "lon": "1.1", + "id": "123", + "accuracy": "10.5", + "batt": 10, + "speed": 100, + "bearing": "105.32", + "altitude": 102, + } + + req = await client.post(url, params=data) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"])) + assert state.state == STATE_NOT_HOME + assert state.attributes["gps_accuracy"] == 10.5 + assert state.attributes["battery_level"] == 10.0 + assert state.attributes["speed"] == 100.0 + assert state.attributes["bearing"] == 105.32 + assert state.attributes["altitude"] == 102.0 + + data = { + "lat": str(HOME_LATITUDE), + "lon": str(HOME_LONGITUDE), + "id": "123", + "accuracy": 123, + "batt": 23, + "speed": 23, + "bearing": 123, + "altitude": 123, + } + + req = await client.post(url, params=data) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"])) + 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["bearing"] == 123 + assert state.attributes["altitude"] == 123 + + +async def test_two_devices(hass, client, webhook_id): + """Test updating two different devices.""" + url = "/api/webhook/{}".format(webhook_id) + + data_device_1 = {"lat": "1.0", "lon": "1.1", "id": "device_1"} + + # Exit Home + req = await client.post(url, params=data_device_1) + await hass.async_block_till_done() + assert req.status == HTTP_OK + + state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data_device_1["id"])) + assert state.state == "not_home" + + # Enter Home + data_device_2 = dict(data_device_1) + data_device_2["lat"] = str(HOME_LATITUDE) + data_device_2["lon"] = str(HOME_LONGITUDE) + data_device_2["id"] = "device_2" + req = await client.post(url, params=data_device_2) + await hass.async_block_till_done() + assert req.status == HTTP_OK + + state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data_device_2["id"])) + assert state.state == "home" + state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data_device_1["id"])) + assert state.state == "not_home" + + +@pytest.mark.xfail( + reason="The device_tracker component does not support unloading yet." +) +async def test_load_unload_entry(hass, client, webhook_id): + """Test that the appropriate dispatch signals are added and removed.""" + url = "/api/webhook/{}".format(webhook_id) + data = {"lat": str(HOME_LATITUDE), "lon": str(HOME_LONGITUDE), "id": "123"} + + # Enter the Home + req = await client.post(url, params=data) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get( + "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"]) + ).state + assert STATE_HOME == state_name + assert len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) == 1 + + entry = hass.config_entries.async_entries(DOMAIN)[0] + + assert await traccar.async_unload_entry(hass, entry) + await hass.async_block_till_done() + assert not hass.data[DATA_DISPATCHER][TRACKER_UPDATE] From fafd2284182ed3a973f1916e41923395d4b47625 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 9 Aug 2019 18:52:47 -0400 Subject: [PATCH 094/273] Refactor ZHA device initialized logic (#25796) * refactor device initialized * better names and update tests * clean up last seen logic * logging consistency --- .../components/zha/core/channels/security.py | 4 +- homeassistant/components/zha/core/gateway.py | 140 ++++++++++++------ tests/components/zha/common.py | 5 +- 3 files changed, 97 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index c9485488d1c..cac93ea7214 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -57,7 +57,7 @@ class IASZoneChannel(ZigbeeChannel): """Configure IAS device.""" # Xiaomi devices don't need this and it disrupts pairing if self._zha_device.manufacturer == "LUMI": - self.debug("%s: finished IASZoneChannel configuration") + self.debug("finished IASZoneChannel configuration") return from zigpy.exceptions import DeliveryError @@ -81,7 +81,7 @@ class IASZoneChannel(ZigbeeChannel): self._cluster.ep_attribute, str(ex), ) - self.debug("%s: finished IASZoneChannel configuration") + self.debug("finished IASZoneChannel configuration") await self.get_attribute_value("zone_type", from_cache=False) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 7adceb13f54..9cf93b56581 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -138,9 +138,7 @@ class ZHAGateway: if device.nwk == 0x0000: continue init_tasks.append( - init_with_semaphore( - self.async_device_initialized(device, False), semaphore - ) + init_with_semaphore(self.async_device_restored(device), semaphore) ) await asyncio.gather(*init_tasks) @@ -181,7 +179,7 @@ class ZHAGateway: def device_initialized(self, device): """Handle device joined and basic information discovered.""" - self._hass.async_create_task(self.async_device_initialized(device, True)) + self._hass.async_create_task(self.async_device_initialized(device)) def device_left(self, device): """Handle device leaving the network.""" @@ -292,7 +290,7 @@ class ZHAGateway: self.debug_enabled = False @callback - def _async_get_or_create_device(self, zigpy_device, is_new_join): + def _async_get_or_create_device(self, zigpy_device): """Get or create a ZHA device.""" zha_device = self._devices.get(zigpy_device.ieee) if zha_device is None: @@ -306,9 +304,6 @@ class ZHAGateway: manufacturer=zha_device.manufacturer, model=zha_device.model, ) - if not is_new_join: - entry = self.zha_storage.async_get_or_create(zha_device) - zha_device.async_update_last_seen(entry.last_seen) return zha_device @callback @@ -333,43 +328,94 @@ class ZHAGateway: self.zha_storage.async_update(device) await self.zha_storage.async_save() - async def async_device_initialized(self, device, is_new_join): + async def async_device_initialized(self, device): """Handle device joined and basic information discovered (async).""" if device.nwk == 0x0000: return - zha_device = self._async_get_or_create_device(device, is_new_join) + zha_device = self._async_get_or_create_device(device) - is_rejoin = False - if zha_device.status is not DeviceStatus.INITIALIZED: - discovery_infos = [] - for endpoint_id, endpoint in device.endpoints.items(): - async_process_endpoint( - self._hass, - self._config, - endpoint_id, - endpoint, - discovery_infos, - device, - zha_device, - is_new_join, - ) - else: - is_rejoin = is_new_join is True + _LOGGER.debug( + "device - %s entering async_device_initialized - is_new_join: %s", + "0x{:04x}:{}".format(device.nwk, device.ieee), + zha_device.status is not DeviceStatus.INITIALIZED, + ) + + if zha_device.status is DeviceStatus.INITIALIZED: + # ZHA already has an initialized device so either the device was assigned a + # new nwk or device was physically reset and added again without being removed _LOGGER.debug( - "skipping discovery for previously discovered device: %s", - "{} - is rejoin: {}".format(zha_device.ieee, is_rejoin), + "device - %s has been reset and readded or its nwk address changed", + "0x{:04x}:{}".format(device.nwk, device.ieee), + ) + await self._async_device_rejoined(zha_device) + else: + _LOGGER.debug( + "device - %s has joined the ZHA zigbee network", + "0x{:04x}:{}".format(device.nwk, device.ieee), + ) + await self._async_device_joined(device, zha_device) + + # This is real traffic from a device so lets update last seen on the entry + entry = self.zha_storage.async_get_or_create(zha_device) + zha_device.async_update_last_seen(entry.last_seen) + + device_info = async_get_device_info( + self._hass, zha_device, self.ha_device_registry + ) + async_dispatcher_send( + self._hass, + ZHA_GW_MSG, + { + ATTR_TYPE: ZHA_GW_MSG_DEVICE_FULL_INIT, + ZHA_GW_MSG_DEVICE_INFO: device_info, + }, + ) + + async def _async_device_joined(self, device, zha_device): + discovery_infos = [] + for endpoint_id, endpoint in device.endpoints.items(): + async_process_endpoint( + self._hass, + self._config, + endpoint_id, + endpoint, + discovery_infos, + device, + zha_device, + True, ) - if is_new_join: - # configure the device - await zha_device.async_configure() - zha_device.update_available(True) - elif zha_device.is_mains_powered: + await zha_device.async_configure() + # will cause async_init to fire so don't explicitly call it + zha_device.update_available(True) + + for discovery_info in discovery_infos: + async_dispatch_discovery_info(self._hass, True, discovery_info) + + # only public for testing + async def async_device_restored(self, device): + """Add an existing device to the ZHA zigbee network when ZHA first starts.""" + zha_device = self._async_get_or_create_device(device) + discovery_infos = [] + for endpoint_id, endpoint in device.endpoints.items(): + async_process_endpoint( + self._hass, + self._config, + endpoint_id, + endpoint, + discovery_infos, + device, + zha_device, + False, + ) + + if zha_device.is_mains_powered: # the device isn't a battery powered device so we should be able # to update it now _LOGGER.debug( - "attempting to request fresh state for %s %s", + "attempting to request fresh state for device - %s %s %s", + "0x{:04x}:{}".format(zha_device.nwk, zha_device.ieee), zha_device.name, "with power source: {}".format(zha_device.power_source), ) @@ -377,22 +423,18 @@ class ZHAGateway: else: await zha_device.async_initialize(from_cache=True) - if not is_rejoin: - for discovery_info in discovery_infos: - async_dispatch_discovery_info(self._hass, is_new_join, discovery_info) + for discovery_info in discovery_infos: + async_dispatch_discovery_info(self._hass, False, discovery_info) - if is_new_join: - device_info = async_get_device_info( - self._hass, zha_device, self.ha_device_registry - ) - async_dispatcher_send( - self._hass, - ZHA_GW_MSG, - { - ATTR_TYPE: ZHA_GW_MSG_DEVICE_FULL_INIT, - ZHA_GW_MSG_DEVICE_INFO: device_info, - }, - ) + async def _async_device_rejoined(self, zha_device): + _LOGGER.debug( + "skipping discovery for previously discovered device - %s", + "0x{:04x}:{}".format(zha_device.nwk, zha_device.ieee), + ) + # we don't have to do this on a nwk swap but we don't have a way to tell currently + await zha_device.async_configure() + # will cause async_init to fire so don't explicitly call it + zha_device.update_available(True) async def shutdown(self): """Stop ZHA Controller Application.""" diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index dd5cade737c..d34c6983528 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -140,7 +140,10 @@ async def async_init_zigpy_device( device = make_device( in_cluster_ids, out_cluster_ids, device_type, ieee, manufacturer, model ) - await gateway.async_device_initialized(device, is_new_join) + if is_new_join: + await gateway.async_device_initialized(device) + else: + await gateway.async_device_restored(device) await hass.async_block_till_done() return device From b79f1336be9dbaaadd1b4084d1d43699a6a6585f Mon Sep 17 00:00:00 2001 From: Tomi Lehto Date: Sat, 10 Aug 2019 03:03:12 +0300 Subject: [PATCH 095/273] Fix 64-bit modbus sensor register reads (#25672) * Fix 64-bit modbus sensor register reads When reading four 16-bit modbus registers as a sensor value, slave output is stored first as 64-bit integer, but before returning that value is converted to double precision floating point. This causes rounding errors for integer values bigger than 2^53. After this change floating point conversion is done only if user has configured scaling or offset using floating points. * Formatting * Review fixes --- homeassistant/components/modbus/sensor.py | 35 +- tests/components/modbus/__init__.py | 1 + tests/components/modbus/test_modbus_sensor.py | 361 ++++++++++++++++++ 3 files changed, 392 insertions(+), 5 deletions(-) create mode 100644 tests/components/modbus/__init__.py create mode 100644 tests/components/modbus/test_modbus_sensor.py diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 43c1a89435f..4fc9fb808c6 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -2,6 +2,7 @@ import logging import struct +from typing import Any, Union import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -36,6 +37,26 @@ DATA_TYPE_UINT = "uint" REGISTER_TYPE_HOLDING = "holding" REGISTER_TYPE_INPUT = "input" + +def number(value: Any) -> Union[int, float]: + """Coerce a value to number without losing precision.""" + if isinstance(value, int): + return value + + if isinstance(value, str): + try: + value = int(value) + return value + except (TypeError, ValueError): + pass + + try: + value = float(value) + return value + except (TypeError, ValueError): + raise vol.Invalid("invalid number {}".format(value)) + + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_REGISTERS): [ @@ -47,13 +68,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( [DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT, DATA_TYPE_CUSTOM] ), vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, - vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float), + vol.Optional(CONF_OFFSET, default=0): number, vol.Optional(CONF_PRECISION, default=0): cv.positive_int, vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING): vol.In( [REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT] ), vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean, - vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), + vol.Optional(CONF_SCALE, default=1): number, vol.Optional(CONF_SLAVE): cv.positive_int, vol.Optional(CONF_STRUCTURE): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, @@ -207,6 +228,10 @@ class ModbusRegisterSensor(RestoreEntity): return byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers]) val = struct.unpack(self._structure, byte_string)[0] - self._value = format( - self._scale * val + self._offset, ".{}f".format(self._precision) - ) + val = self._scale * val + self._offset + if isinstance(val, int): + self._value = str(val) + if self._precision > 0: + self._value += "." + "0" * self._precision + else: + self._value = f"{val:.{self._precision}f}" diff --git a/tests/components/modbus/__init__.py b/tests/components/modbus/__init__.py new file mode 100644 index 00000000000..86f5641c475 --- /dev/null +++ b/tests/components/modbus/__init__.py @@ -0,0 +1 @@ +"""The tests for Modbus platforms.""" diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py new file mode 100644 index 00000000000..82d0b4bd5f0 --- /dev/null +++ b/tests/components/modbus/test_modbus_sensor.py @@ -0,0 +1,361 @@ +"""The tests for the Modbus sensor component.""" +import pytest +from datetime import timedelta +from unittest import mock + +from homeassistant.const import ( + CONF_NAME, + CONF_OFFSET, + CONF_PLATFORM, + CONF_SCAN_INTERVAL, +) +from homeassistant.components.modbus import DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN +from homeassistant.components.modbus.sensor import ( + CONF_COUNT, + CONF_DATA_TYPE, + CONF_PRECISION, + CONF_REGISTER, + CONF_REGISTER_TYPE, + CONF_REGISTERS, + CONF_REVERSE_ORDER, + CONF_SCALE, + DATA_TYPE_FLOAT, + DATA_TYPE_INT, + DATA_TYPE_UINT, + REGISTER_TYPE_HOLDING, + REGISTER_TYPE_INPUT, +) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util +from tests.common import MockModule, mock_integration, async_fire_time_changed + + +@pytest.fixture() +def mock_hub(hass): + """Mock hub.""" + mock_integration(hass, MockModule(MODBUS_DOMAIN)) + hub = mock.MagicMock() + hub.name = "hub" + hass.data[MODBUS_DOMAIN] = {DEFAULT_HUB: hub} + return hub + + +common_register_config = {CONF_NAME: "test-config", CONF_REGISTER: 1234} + + +class ReadResult: + """Storage class for register read results.""" + + def __init__(self, register_words): + """Init.""" + self.registers = register_words + + +async def run_test(hass, mock_hub, register_config, register_words, expected): + """Run test for given config and check that sensor outputs expected result.""" + + # Full sensor configuration + sensor_name = "modbus_test_sensor" + scan_interval = 5 + config = { + SENSOR_DOMAIN: { + CONF_PLATFORM: "modbus", + CONF_SCAN_INTERVAL: scan_interval, + CONF_REGISTERS: [ + dict(**{CONF_NAME: sensor_name, CONF_REGISTER: 1234}, **register_config) + ], + } + } + + # Setup inputs for the sensor + read_result = ReadResult(register_words) + if register_config.get(CONF_REGISTER_TYPE) == REGISTER_TYPE_INPUT: + mock_hub.read_input_registers.return_value = read_result + else: + mock_hub.read_holding_registers.return_value = read_result + + # Initialize sensor + now = dt_util.utcnow() + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + + # Trigger update call with time_changed event + now += timedelta(seconds=scan_interval + 1) + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # Check state + entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" + state = hass.states.get(entity_id).state + assert state == expected + + +async def test_simple_word_register(hass, mock_hub): + """Test conversion of single word register.""" + register_config = { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + } + await run_test(hass, mock_hub, register_config, register_words=[0], expected="0") + + +async def test_optional_conf_keys(hass, mock_hub): + """Test handling of optional configuration keys.""" + register_config = {} + await run_test( + hass, mock_hub, register_config, register_words=[0x8000], expected="-32768" + ) + + +async def test_offset(hass, mock_hub): + """Test offset calculation.""" + register_config = { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 1, + CONF_OFFSET: 13, + CONF_PRECISION: 0, + } + await run_test(hass, mock_hub, register_config, register_words=[7], expected="20") + + +async def test_scale_and_offset(hass, mock_hub): + """Test handling of scale and offset.""" + register_config = { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 3, + CONF_OFFSET: 13, + CONF_PRECISION: 0, + } + await run_test(hass, mock_hub, register_config, register_words=[7], expected="34") + + +async def test_ints_can_have_precision(hass, mock_hub): + """Test precision can be specified event if using integer values only.""" + register_config = { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 3, + CONF_OFFSET: 13, + CONF_PRECISION: 4, + } + await run_test( + hass, mock_hub, register_config, register_words=[7], expected="34.0000" + ) + + +async def test_floats_get_rounded_correctly(hass, mock_hub): + """Test that floating point values get rounded correctly.""" + register_config = { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 1.5, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + } + await run_test(hass, mock_hub, register_config, register_words=[1], expected="2") + + +async def test_parameters_as_strings(hass, mock_hub): + """Test that scale, offset and precision can be given as strings.""" + register_config = { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: "1.5", + CONF_OFFSET: "5", + CONF_PRECISION: "1", + } + await run_test(hass, mock_hub, register_config, register_words=[9], expected="18.5") + + +async def test_floating_point_scale(hass, mock_hub): + """Test use of floating point scale.""" + register_config = { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 2.4, + CONF_OFFSET: 0, + CONF_PRECISION: 2, + } + await run_test(hass, mock_hub, register_config, register_words=[1], expected="2.40") + + +async def test_floating_point_offset(hass, mock_hub): + """Test use of floating point scale.""" + register_config = { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 1, + CONF_OFFSET: -10.3, + CONF_PRECISION: 1, + } + await run_test(hass, mock_hub, register_config, register_words=[2], expected="-8.3") + + +async def test_signed_two_word_register(hass, mock_hub): + """Test reading of signed register with two words.""" + register_config = { + CONF_COUNT: 2, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + } + await run_test( + hass, + mock_hub, + register_config, + register_words=[0x89AB, 0xCDEF], + expected="-1985229329", + ) + + +async def test_unsigned_two_word_register(hass, mock_hub): + """Test reading of unsigned register with two words.""" + register_config = { + CONF_COUNT: 2, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + } + await run_test( + hass, + mock_hub, + register_config, + register_words=[0x89AB, 0xCDEF], + expected=str(0x89ABCDEF), + ) + + +async def test_reversed(hass, mock_hub): + """Test handling of reversed register words.""" + register_config = { + CONF_COUNT: 2, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_REVERSE_ORDER: True, + } + await run_test( + hass, + mock_hub, + register_config, + register_words=[0x89AB, 0xCDEF], + expected=str(0xCDEF89AB), + ) + + +async def test_four_word_register(hass, mock_hub): + """Test reading of 64-bit register.""" + register_config = { + CONF_COUNT: 4, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + } + await run_test( + hass, + mock_hub, + register_config, + register_words=[0x89AB, 0xCDEF, 0x0123, 0x4567], + expected="9920249030613615975", + ) + + +async def test_four_word_register_precision_is_intact_with_int_params(hass, mock_hub): + """Test that precision is not lost when doing integer arithmetic for 64-bit register.""" + register_config = { + CONF_COUNT: 4, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 2, + CONF_OFFSET: 3, + CONF_PRECISION: 0, + } + await run_test( + hass, + mock_hub, + register_config, + register_words=[0x0123, 0x4567, 0x89AB, 0xCDEF], + expected="163971058432973793", + ) + + +async def test_four_word_register_precision_is_lost_with_float_params(hass, mock_hub): + """Test that precision is affected when floating point conversion is done.""" + register_config = { + CONF_COUNT: 4, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 2.0, + CONF_OFFSET: 3.0, + CONF_PRECISION: 0, + } + await run_test( + hass, + mock_hub, + register_config, + register_words=[0x0123, 0x4567, 0x89AB, 0xCDEF], + expected="163971058432973792", + ) + + +async def test_two_word_input_register(hass, mock_hub): + """Test reaging of input register.""" + register_config = { + CONF_COUNT: 2, + CONF_REGISTER_TYPE: REGISTER_TYPE_INPUT, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + } + await run_test( + hass, + mock_hub, + register_config, + register_words=[0x89AB, 0xCDEF], + expected=str(0x89ABCDEF), + ) + + +async def test_two_word_holding_register(hass, mock_hub): + """Test reaging of holding register.""" + register_config = { + CONF_COUNT: 2, + CONF_REGISTER_TYPE: REGISTER_TYPE_HOLDING, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + } + await run_test( + hass, + mock_hub, + register_config, + register_words=[0x89AB, 0xCDEF], + expected=str(0x89ABCDEF), + ) + + +async def test_float_data_type(hass, mock_hub): + """Test floating point register data type.""" + register_config = { + CONF_COUNT: 2, + CONF_REGISTER_TYPE: REGISTER_TYPE_HOLDING, + CONF_DATA_TYPE: DATA_TYPE_FLOAT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 5, + } + await run_test( + hass, + mock_hub, + register_config, + register_words=[16286, 1617], + expected="1.23457", + ) From f30e54f01e0460f08ff3d470ca45612eb9d860d8 Mon Sep 17 00:00:00 2001 From: Cameron Morris <636871+cameronrmorris@users.noreply.github.com> Date: Fri, 9 Aug 2019 20:05:05 -0400 Subject: [PATCH 096/273] Fix eco preset for Wink Air Conditioner (#25763) * Add preset support for device * Provide mappings between preset changes --- homeassistant/components/wink/climate.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wink/climate.py b/homeassistant/components/wink/climate.py index ed2d2482802..38f25ef0a83 100644 --- a/homeassistant/components/wink/climate.py +++ b/homeassistant/components/wink/climate.py @@ -27,6 +27,7 @@ from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, + SUPPORT_PRESET_MODE, PRESET_NONE, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS @@ -62,7 +63,7 @@ SUPPORT_FLAGS_THERMOSTAT = ( SUPPORT_FAN_THERMOSTAT = [FAN_AUTO, FAN_ON] SUPPORT_PRESET_THERMOSTAT = [PRESET_AWAY, PRESET_ECO] -SUPPORT_FLAGS_AC = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE +SUPPORT_FLAGS_AC = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_PRESET_MODE SUPPORT_FAN_AC = [FAN_HIGH, FAN_LOW, FAN_MEDIUM] SUPPORT_PRESET_AC = [PRESET_NONE, PRESET_ECO] @@ -415,10 +416,13 @@ class WinkAC(WinkDevice, ClimateDevice): @property def preset_mode(self): """Return the current preset mode, e.g., home, away, temp.""" + if not self.wink.is_on(): + return PRESET_NONE + mode = self.wink.current_mode() if mode == "auto_eco": return PRESET_ECO - return None + return PRESET_NONE @property def preset_modes(self): @@ -436,7 +440,7 @@ class WinkAC(WinkDevice, ClimateDevice): wink_mode = self.wink.current_mode() if wink_mode == "auto_eco": - return HVAC_MODE_AUTO + return HVAC_MODE_COOL return WINK_HVAC_TO_HA.get(wink_mode) @property @@ -476,6 +480,8 @@ class WinkAC(WinkDevice, ClimateDevice): """Set new preset mode.""" if preset_mode == PRESET_ECO: self.wink.set_operation_mode("auto_eco") + elif self.hvac_mode == HVAC_MODE_COOL and preset_mode == PRESET_NONE: + self.set_hvac_mode(HVAC_MODE_COOL) @property def target_temperature(self): From 12f4076f70b9ebf4a7edc92ba78b4cabd30ab357 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 10 Aug 2019 12:15:36 +0200 Subject: [PATCH 097/273] Update Dockerfile --- .devcontainer/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 8abf28cddff..021f2bfa1b0 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -20,10 +20,10 @@ RUN git clone --depth 1 https://github.com/home-assistant/hass-release \ && cd hass-release \ && pip3 install -e . -WORKDIR /workspace +WORKDIR /workspaces # Install Python dependencies from requirements.txt if it exists -COPY requirements_test_all.txt homeassistant/package_constraints.txt /workspace/ +COPY requirements_test_all.txt homeassistant/package_constraints.txt . RUN pip3 install -r requirements_test_all.txt -c package_constraints.txt # Set the default shell to bash instead of sh From e685f077aec5fef455c96ddb82afd93fd2fcaebb Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 10 Aug 2019 12:27:32 +0200 Subject: [PATCH 098/273] Update Dockerfile --- .devcontainer/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 021f2bfa1b0..3bfc7e94148 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -23,7 +23,7 @@ RUN git clone --depth 1 https://github.com/home-assistant/hass-release \ WORKDIR /workspaces # Install Python dependencies from requirements.txt if it exists -COPY requirements_test_all.txt homeassistant/package_constraints.txt . +COPY requirements_test_all.txt homeassistant/package_constraints.txt /workspaces/ RUN pip3 install -r requirements_test_all.txt -c package_constraints.txt # Set the default shell to bash instead of sh From 9e6732e53002a54489fff86efc5cd00c881e220c Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Sat, 10 Aug 2019 20:50:27 +1000 Subject: [PATCH 099/273] GeoNet NZ Quakes feed integration (#25736) * initial working version * support configured unit system and convert distance automatically * properly unloading integration when removed and stopping refreshes * ran isort * fixed pylint * introduced time filter of seven days into past * adding unit tests * fixed lint * removed unused code * added test case * added test case for config flow * fixed lint * fixed comment * removed unused test code * increased test coverage * fixed filtering by time * changed wording in config flow * reformatted with black * removed unused logger * fixed black * changed default mmi * reduced the options in the config flow form; fixed a few schema options and processing of data * moved unsubscribing signals * fixed minimum magnitude and modified tests * fixed radius in imperial unit system * increased test coverage * simplified code * fixed lint * changed string formatting; simplified code * removed unused strings * added translation --- CODEOWNERS | 1 + .../geonetnz_quakes/.translations/en.json | 17 ++ .../components/geonetnz_quakes/__init__.py | 101 +++++++ .../components/geonetnz_quakes/config_flow.py | 94 ++++++ .../components/geonetnz_quakes/const.py | 14 + .../geonetnz_quakes/geo_location.py | 284 ++++++++++++++++++ .../components/geonetnz_quakes/manifest.json | 13 + .../components/geonetnz_quakes/strings.json | 17 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/geonetnz_quakes/__init__.py | 1 + .../geonetnz_quakes/test_config_flow.py | 135 +++++++++ .../geonetnz_quakes/test_geo_location.py | 223 ++++++++++++++ 15 files changed, 908 insertions(+) create mode 100644 homeassistant/components/geonetnz_quakes/.translations/en.json create mode 100644 homeassistant/components/geonetnz_quakes/__init__.py create mode 100644 homeassistant/components/geonetnz_quakes/config_flow.py create mode 100644 homeassistant/components/geonetnz_quakes/const.py create mode 100644 homeassistant/components/geonetnz_quakes/geo_location.py create mode 100644 homeassistant/components/geonetnz_quakes/manifest.json create mode 100644 homeassistant/components/geonetnz_quakes/strings.json create mode 100644 tests/components/geonetnz_quakes/__init__.py create mode 100644 tests/components/geonetnz_quakes/test_config_flow.py create mode 100644 tests/components/geonetnz_quakes/test_geo_location.py diff --git a/CODEOWNERS b/CODEOWNERS index 65a12177c07..ff565d79d9d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -102,6 +102,7 @@ homeassistant/components/fronius/* @nielstron homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/gearbest/* @HerrHofrat homeassistant/components/geniushub/* @zxdavb +homeassistant/components/geonetnz_quakes/* @exxamalte homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff homeassistant/components/gntp/* @robbiet480 diff --git a/homeassistant/components/geonetnz_quakes/.translations/en.json b/homeassistant/components/geonetnz_quakes/.translations/en.json new file mode 100644 index 00000000000..4143efcdf96 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/en.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Location already registered" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + }, + "title": "Fill in your filter details." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/__init__.py b/homeassistant/components/geonetnz_quakes/__init__.py new file mode 100644 index 00000000000..e786b413029 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/__init__.py @@ -0,0 +1,101 @@ +"""The GeoNet NZ Quakes integration.""" +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, +) +from homeassistant.helpers import config_validation as cv + +from .config_flow import configured_instances +from .const import ( + CONF_MINIMUM_MAGNITUDE, + CONF_MMI, + DEFAULT_MINIMUM_MAGNITUDE, + DEFAULT_MMI, + DEFAULT_RADIUS, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + FEED, +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_MMI, default=DEFAULT_MMI): vol.All( + vol.Coerce(int), vol.Range(min=-1, max=8) + ), + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.Coerce(float), + vol.Optional( + CONF_MINIMUM_MAGNITUDE, default=DEFAULT_MINIMUM_MAGNITUDE + ): vol.All(vol.Coerce(float), vol.Range(min=0)), + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the GeoNet NZ Quakes component.""" + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + + latitude = conf.get(CONF_LATITUDE, hass.config.latitude) + longitude = conf.get(CONF_LONGITUDE, hass.config.longitude) + mmi = conf[CONF_MMI] + scan_interval = conf[CONF_SCAN_INTERVAL] + + identifier = f"{latitude}, {longitude}" + if identifier in configured_instances(hass): + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_LATITUDE: latitude, + CONF_LONGITUDE: longitude, + CONF_RADIUS: conf[CONF_RADIUS], + CONF_MINIMUM_MAGNITUDE: conf[CONF_MINIMUM_MAGNITUDE], + CONF_MMI: mmi, + CONF_SCAN_INTERVAL: scan_interval, + }, + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up the GeoNet NZ Quakes component as config entry.""" + hass.data[DOMAIN] = {} + hass.data[DOMAIN][FEED] = {} + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "geo_location") + ) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload an GeoNet NZ Quakes component config entry.""" + manager = hass.data[DOMAIN][FEED].pop(config_entry.entry_id) + await manager.async_stop() + + await hass.config_entries.async_forward_entry_unload(config_entry, "geo_location") + + return True diff --git a/homeassistant/components/geonetnz_quakes/config_flow.py b/homeassistant/components/geonetnz_quakes/config_flow.py new file mode 100644 index 00000000000..bd93f08c72b --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/config_flow.py @@ -0,0 +1,94 @@ +"""Config flow to configure the GeoNet NZ Quakes integration.""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, +) +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv + +from .const import ( + CONF_MMI, + DEFAULT_MMI, + DEFAULT_RADIUS, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + DEFAULT_MINIMUM_MAGNITUDE, + CONF_MINIMUM_MAGNITUDE, +) + +_LOGGER = logging.getLogger(__name__) + + +@callback +def configured_instances(hass): + """Return a set of configured GeoNet NZ Quakes instances.""" + return set( + f"{entry.data[CONF_LATITUDE]}, {entry.data[CONF_LONGITUDE]}" + for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +@config_entries.HANDLERS.register(DOMAIN) +class GeonetnzQuakesFlowHandler(config_entries.ConfigFlow): + """Handle a GeoNet NZ Quakes config flow.""" + + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def _show_form(self, errors=None): + """Show the form to the user.""" + data_schema = vol.Schema( + { + vol.Optional(CONF_MMI, default=DEFAULT_MMI): vol.All( + vol.Coerce(int), vol.Range(min=-1, max=8) + ), + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): cv.positive_int, + } + ) + + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors or {} + ) + + 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.""" + _LOGGER.debug("User input: %s", user_input) + if not user_input: + return await self._show_form() + + latitude = user_input.get(CONF_LATITUDE, self.hass.config.latitude) + user_input[CONF_LATITUDE] = latitude + longitude = user_input.get(CONF_LONGITUDE, self.hass.config.longitude) + user_input[CONF_LONGITUDE] = longitude + + identifier = f"{user_input[CONF_LATITUDE]}, {user_input[CONF_LONGITUDE]}" + if identifier in configured_instances(self.hass): + return await self._show_form({"base": "identifier_exists"}) + + if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + user_input[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_IMPERIAL + else: + user_input[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_METRIC + + scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + user_input[CONF_SCAN_INTERVAL] = scan_interval.seconds + + minimum_magnitude = user_input.get( + CONF_MINIMUM_MAGNITUDE, DEFAULT_MINIMUM_MAGNITUDE + ) + user_input[CONF_MINIMUM_MAGNITUDE] = minimum_magnitude + + return self.async_create_entry(title=identifier, data=user_input) diff --git a/homeassistant/components/geonetnz_quakes/const.py b/homeassistant/components/geonetnz_quakes/const.py new file mode 100644 index 00000000000..d06e85ee2cb --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/const.py @@ -0,0 +1,14 @@ +"""Define constants for the GeoNet NZ Quakes integration.""" +from datetime import timedelta + +DOMAIN = "geonetnz_quakes" + +CONF_MINIMUM_MAGNITUDE = "minimum_magnitude" +CONF_MMI = "mmi" + +FEED = "feed" + +DEFAULT_MINIMUM_MAGNITUDE = 0.0 +DEFAULT_MMI = 3 +DEFAULT_RADIUS = 50.0 +DEFAULT_SCAN_INTERVAL = timedelta(minutes=5) diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py new file mode 100644 index 00000000000..9d4be94e3aa --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/geo_location.py @@ -0,0 +1,284 @@ +"""Geolocation support for GeoNet NZ Quakes Feeds.""" +from datetime import timedelta +import logging +from typing import Optional + +from aio_geojson_geonetnz_quakes import GeonetnzQuakesFeedManager + +from homeassistant.components.geo_location import GeolocationEvent +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_KILOMETERS, + LENGTH_MILES, + ATTR_TIME, +) +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM + +from .const import CONF_MINIMUM_MAGNITUDE, CONF_MMI, DOMAIN, FEED + +_LOGGER = logging.getLogger(__name__) + +ATTR_DEPTH = "depth" +ATTR_EXTERNAL_ID = "external_id" +ATTR_LOCALITY = "locality" +ATTR_MAGNITUDE = "magnitude" +ATTR_MMI = "mmi" +ATTR_PUBLICATION_DATE = "publication_date" +ATTR_QUALITY = "quality" + +DEFAULT_FILTER_TIME_INTERVAL = timedelta(days=7) + +SIGNAL_DELETE_ENTITY = "geonetnz_quakes_delete_{}" +SIGNAL_UPDATE_ENTITY = "geonetnz_quakes_update_{}" + +SOURCE = "geonetnz_quakes" + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the GeoNet NZ Quakes Feed platform.""" + radius = entry.data[CONF_RADIUS] + unit_system = entry.data[CONF_UNIT_SYSTEM] + if unit_system == CONF_UNIT_SYSTEM_IMPERIAL: + radius = METRIC_SYSTEM.length(radius, LENGTH_MILES) + manager = GeonetnzQuakesFeedEntityManager( + hass, + async_add_entities, + entry.data[CONF_SCAN_INTERVAL], + entry.data[CONF_LATITUDE], + entry.data[CONF_LONGITUDE], + entry.data[CONF_MMI], + radius, + unit_system, + entry.data[CONF_MINIMUM_MAGNITUDE], + ) + hass.data[DOMAIN][FEED][entry.entry_id] = manager + await manager.async_init() + + +class GeonetnzQuakesFeedEntityManager: + """Feed Entity Manager for GeoNet NZ Quakes feed.""" + + def __init__( + self, + hass, + async_add_entities, + scan_interval, + latitude, + longitude, + mmi, + radius_in_km, + unit_system, + minimum_magnitude, + ): + """Initialize the Feed Entity Manager.""" + self._hass = hass + coordinates = (latitude, longitude) + websession = aiohttp_client.async_get_clientsession(hass) + self._feed_manager = GeonetnzQuakesFeedManager( + websession, + self._generate_entity, + self._update_entity, + self._remove_entity, + coordinates, + mmi=mmi, + filter_radius=radius_in_km, + filter_minimum_magnitude=minimum_magnitude, + filter_time=DEFAULT_FILTER_TIME_INTERVAL, + ) + self._async_add_entities = async_add_entities + self._scan_interval = timedelta(seconds=scan_interval) + self._unit_system = unit_system + self._track_time_remove_callback = None + + async def async_init(self): + """Schedule regular updates based on configured time interval.""" + + async def update(event_time): + """Update.""" + await self.async_update() + + await self.async_update() + self._track_time_remove_callback = async_track_time_interval( + self._hass, update, self._scan_interval + ) + _LOGGER.debug("Feed entity manager initialized") + + async def async_update(self): + """Refresh data.""" + await self._feed_manager.update() + _LOGGER.debug("Feed entity manager updated") + + async def async_stop(self): + """Stop this feed entity manager from refreshing.""" + if self._track_time_remove_callback: + self._track_time_remove_callback() + _LOGGER.debug("Feed entity manager stopped") + + def get_entry(self, external_id): + """Get feed entry by external id.""" + return self._feed_manager.feed_entries.get(external_id) + + async def _generate_entity(self, external_id): + """Generate new entity.""" + new_entity = GeonetnzQuakesEvent(self, external_id, self._unit_system) + # Add new entities to HA. + self._async_add_entities([new_entity], True) + + async def _update_entity(self, external_id): + """Update entity.""" + async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) + + async def _remove_entity(self, external_id): + """Remove entity.""" + async_dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) + + +class GeonetnzQuakesEvent(GeolocationEvent): + """This represents an external event with GeoNet NZ Quakes feed data.""" + + def __init__(self, feed_manager, external_id, unit_system): + """Initialize entity with data from feed entry.""" + self._feed_manager = feed_manager + self._external_id = external_id + self._unit_system = unit_system + self._title = None + self._distance = None + self._latitude = None + self._longitude = None + self._attribution = None + self._depth = None + self._locality = None + self._magnitude = None + self._mmi = None + self._quality = None + self._time = None + self._remove_signal_delete = None + self._remove_signal_update = None + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + self._remove_signal_delete = async_dispatcher_connect( + self.hass, + SIGNAL_DELETE_ENTITY.format(self._external_id), + self._delete_callback, + ) + self._remove_signal_update = async_dispatcher_connect( + self.hass, + SIGNAL_UPDATE_ENTITY.format(self._external_id), + self._update_callback, + ) + + async def async_will_remove_from_hass(self) -> None: + """Call when entity will be removed from hass.""" + self._remove_signal_delete() + self._remove_signal_update() + + @callback + def _delete_callback(self): + """Remove this entity.""" + self.hass.async_create_task(self.async_remove()) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + + @property + def should_poll(self): + """No polling needed for GeoNet NZ Quakes feed location events.""" + return False + + async def async_update(self): + """Update this entity from the data held in the feed manager.""" + _LOGGER.debug("Updating %s", self._external_id) + feed_entry = self._feed_manager.get_entry(self._external_id) + if feed_entry: + self._update_from_feed(feed_entry) + + def _update_from_feed(self, feed_entry): + """Update the internal state from the provided feed entry.""" + self._title = feed_entry.title + # Convert distance if not metric system. + if self._unit_system == CONF_UNIT_SYSTEM_IMPERIAL: + self._distance = IMPERIAL_SYSTEM.length( + feed_entry.distance_to_home, LENGTH_KILOMETERS + ) + else: + self._distance = feed_entry.distance_to_home + self._latitude = feed_entry.coordinates[0] + self._longitude = feed_entry.coordinates[1] + self._attribution = feed_entry.attribution + self._depth = feed_entry.depth + self._locality = feed_entry.locality + self._magnitude = feed_entry.magnitude + self._mmi = feed_entry.mmi + self._quality = feed_entry.quality + self._time = feed_entry.time + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return "mdi:pulse" + + @property + def source(self) -> str: + """Return source value of this external event.""" + return SOURCE + + @property + def name(self) -> Optional[str]: + """Return the name of the entity.""" + return self._title + + @property + def distance(self) -> Optional[float]: + """Return distance value of this external event.""" + return self._distance + + @property + def latitude(self) -> Optional[float]: + """Return latitude value of this external event.""" + return self._latitude + + @property + def longitude(self) -> Optional[float]: + """Return longitude value of this external event.""" + return self._longitude + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + if self._unit_system == CONF_UNIT_SYSTEM_IMPERIAL: + return LENGTH_MILES + return LENGTH_KILOMETERS + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = {} + for key, value in ( + (ATTR_EXTERNAL_ID, self._external_id), + (ATTR_ATTRIBUTION, self._attribution), + (ATTR_DEPTH, self._depth), + (ATTR_LOCALITY, self._locality), + (ATTR_MAGNITUDE, self._magnitude), + (ATTR_MMI, self._mmi), + (ATTR_QUALITY, self._quality), + (ATTR_TIME, self._time), + ): + if value or isinstance(value, bool): + attributes[key] = value + return attributes diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json new file mode 100644 index 00000000000..44842133021 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "geonetnz_quakes", + "name": "GeoNet NZ Quakes", + "config_flow": true, + "documentation": "https://www.home-assistant.io/components/geonetnz_quakes", + "requirements": [ + "aio_geojson_geonetnz_quakes==0.5" + ], + "dependencies": [], + "codeowners": [ + "@exxamalte" + ] +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/strings.json b/homeassistant/components/geonetnz_quakes/strings.json new file mode 100644 index 00000000000..6ec915eb68d --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/strings.json @@ -0,0 +1,17 @@ +{ + "config": { + "title": "GeoNet NZ Quakes", + "step": { + "user": { + "title": "Fill in your filter details.", + "data": { + "radius": "Radius", + "mmi": "MMI" + } + } + }, + "error": { + "identifier_exists": "Location already registered" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 497686d0a2e..de665ecf5a6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -16,6 +16,7 @@ FLOWS = [ "emulated_roku", "esphome", "geofency", + "geonetnz_quakes", "gpslogger", "hangouts", "heos", diff --git a/requirements_all.txt b/requirements_all.txt index 46d72fc8d19..66d6cba8eba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -114,6 +114,9 @@ adguardhome==0.2.1 # homeassistant.components.frontier_silicon afsapi==0.0.4 +# homeassistant.components.geonetnz_quakes +aio_geojson_geonetnz_quakes==0.5 + # homeassistant.components.ambient_station aioambient==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0621a28221..cb63f138d36 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -42,6 +42,9 @@ YesssSMS==0.2.3 # homeassistant.components.adguard adguardhome==0.2.1 +# homeassistant.components.geonetnz_quakes +aio_geojson_geonetnz_quakes==0.5 + # homeassistant.components.ambient_station aioambient==0.3.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index edf74b93793..9417b926423 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -43,6 +43,7 @@ COMMENT_REQUIREMENTS = ( TEST_REQUIREMENTS = ( "adguardhome", "ambiclimate", + "aio_geojson_geonetnz_quakes", "aioambient", "aioautomatic", "aiobotocore", diff --git a/tests/components/geonetnz_quakes/__init__.py b/tests/components/geonetnz_quakes/__init__.py new file mode 100644 index 00000000000..95c50679338 --- /dev/null +++ b/tests/components/geonetnz_quakes/__init__.py @@ -0,0 +1 @@ +"""Tests for the geonetnz_quakes component.""" diff --git a/tests/components/geonetnz_quakes/test_config_flow.py b/tests/components/geonetnz_quakes/test_config_flow.py new file mode 100644 index 00000000000..2d8e3750648 --- /dev/null +++ b/tests/components/geonetnz_quakes/test_config_flow.py @@ -0,0 +1,135 @@ +"""Define tests for the GeoNet NZ Quakes config flow.""" +from datetime import timedelta + +import pytest +from asynctest import patch, CoroutineMock + +from homeassistant import data_entry_flow +from homeassistant.components.geonetnz_quakes import ( + async_setup_entry, + config_flow, + CONF_MMI, + CONF_MINIMUM_MAGNITUDE, + DOMAIN, + async_unload_entry, + FEED, +) +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_UNIT_SYSTEM, + CONF_SCAN_INTERVAL, +) +from tests.common import MockConfigEntry + + +@pytest.fixture +def config_entry(): + """Create a mock GeoNet NZ Quakes config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LATITUDE: -41.2, + CONF_LONGITUDE: 174.7, + CONF_RADIUS: 25, + CONF_UNIT_SYSTEM: "metric", + CONF_SCAN_INTERVAL: 300.0, + CONF_MMI: 4, + CONF_MINIMUM_MAGNITUDE: 0.0, + }, + title="-41.2, 174.7", + ) + + +async def test_duplicate_error(hass, config_entry): + """Test that errors are shown when duplicates are added.""" + conf = {CONF_LATITUDE: -41.2, CONF_LONGITUDE: 174.7, CONF_RADIUS: 25} + + config_entry.add_to_hass(hass) + flow = config_flow.GeonetnzQuakesFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result["errors"] == {"base": "identifier_exists"} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + flow = config_flow.GeonetnzQuakesFlowHandler() + 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): + """Test that the import step works.""" + conf = { + CONF_LATITUDE: -41.2, + CONF_LONGITUDE: 174.7, + CONF_RADIUS: 25, + CONF_UNIT_SYSTEM: "metric", + CONF_MMI: 2, + CONF_SCAN_INTERVAL: timedelta(minutes=4), + CONF_MINIMUM_MAGNITUDE: 2.5, + } + + flow = config_flow.GeonetnzQuakesFlowHandler() + 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"] == "-41.2, 174.7" + assert result["data"] == { + CONF_LATITUDE: -41.2, + CONF_LONGITUDE: 174.7, + CONF_RADIUS: 25, + CONF_MMI: 2, + CONF_UNIT_SYSTEM: "metric", + CONF_SCAN_INTERVAL: 240.0, + CONF_MINIMUM_MAGNITUDE: 2.5, + } + + +async def test_step_user(hass): + """Test that the user step works.""" + hass.config.latitude = -41.2 + hass.config.longitude = 174.7 + conf = {CONF_RADIUS: 25, CONF_MMI: 4} + + flow = config_flow.GeonetnzQuakesFlowHandler() + 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"] == "-41.2, 174.7" + assert result["data"] == { + CONF_LATITUDE: -41.2, + CONF_LONGITUDE: 174.7, + CONF_RADIUS: 25, + CONF_MMI: 4, + CONF_UNIT_SYSTEM: "metric", + CONF_SCAN_INTERVAL: 300.0, + CONF_MINIMUM_MAGNITUDE: 0.0, + } + + +async def test_component_unload_config_entry(hass, config_entry): + """Test that loading and unloading of a config entry works.""" + config_entry.add_to_hass(hass) + with patch( + "aio_geojson_geonetnz_quakes.GeonetnzQuakesFeedManager.update", + new_callable=CoroutineMock, + ) as mock_feed_manager_update: + # Load config entry. + assert await async_setup_entry(hass, config_entry) + await hass.async_block_till_done() + assert mock_feed_manager_update.call_count == 1 + assert hass.data[DOMAIN][FEED][config_entry.entry_id] is not None + # Unload config entry. + assert await async_unload_entry(hass, config_entry) + await hass.async_block_till_done() + assert hass.data[DOMAIN][FEED].get(config_entry.entry_id) is None diff --git a/tests/components/geonetnz_quakes/test_geo_location.py b/tests/components/geonetnz_quakes/test_geo_location.py new file mode 100644 index 00000000000..c5b7282f320 --- /dev/null +++ b/tests/components/geonetnz_quakes/test_geo_location.py @@ -0,0 +1,223 @@ +"""The tests for the GeoNet NZ Quakes Feed integration.""" +import datetime +from unittest.mock import MagicMock + +from asynctest import patch, CoroutineMock + +from homeassistant.components import geonetnz_quakes +from homeassistant.components.geo_location import ATTR_SOURCE +from homeassistant.components.geonetnz_quakes import DEFAULT_SCAN_INTERVAL +from homeassistant.components.geonetnz_quakes.geo_location import ( + ATTR_EXTERNAL_ID, + ATTR_MAGNITUDE, + ATTR_LOCALITY, + ATTR_MMI, + ATTR_DEPTH, + ATTR_QUALITY, +) +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, + CONF_RADIUS, + ATTR_LATITUDE, + ATTR_LONGITUDE, + ATTR_FRIENDLY_NAME, + ATTR_UNIT_OF_MEASUREMENT, + ATTR_ATTRIBUTION, + ATTR_TIME, + ATTR_ICON, +) +from homeassistant.setup import async_setup_component +from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from tests.common import async_fire_time_changed +import homeassistant.util.dt as dt_util + +CONFIG = {geonetnz_quakes.DOMAIN: {CONF_RADIUS: 200}} + + +def _generate_mock_feed_entry( + external_id, + title, + distance_to_home, + coordinates, + attribution=None, + depth=None, + magnitude=None, + mmi=None, + locality=None, + quality=None, + time=None, +): + """Construct a mock feed entry for testing purposes.""" + feed_entry = MagicMock() + feed_entry.external_id = external_id + feed_entry.title = title + feed_entry.distance_to_home = distance_to_home + feed_entry.coordinates = coordinates + feed_entry.attribution = attribution + feed_entry.depth = depth + feed_entry.magnitude = magnitude + feed_entry.mmi = mmi + feed_entry.locality = locality + feed_entry.quality = quality + feed_entry.time = time + return feed_entry + + +async def test_setup(hass): + """Test the general setup of the integration.""" + # Set up some mock feed entries for this test. + mock_entry_1 = _generate_mock_feed_entry( + "1234", + "Title 1", + 15.5, + (38.0, -3.0), + locality="Locality 1", + attribution="Attribution 1", + time=datetime.datetime(2018, 9, 22, 8, 0, tzinfo=datetime.timezone.utc), + magnitude=5.7, + mmi=5, + depth=10.5, + quality="best", + ) + mock_entry_2 = _generate_mock_feed_entry( + "2345", "Title 2", 20.5, (38.1, -3.1), magnitude=4.6 + ) + mock_entry_3 = _generate_mock_feed_entry( + "3456", "Title 3", 25.5, (38.2, -3.2), locality="Locality 3" + ) + mock_entry_4 = _generate_mock_feed_entry("4567", "Title 4", 12.5, (38.3, -3.3)) + + # Patching 'utcnow' to gain more control over the timed update. + utcnow = dt_util.utcnow() + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=CoroutineMock + ) as mock_feed_update: + mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] + assert await async_setup_component(hass, geonetnz_quakes.DOMAIN, CONFIG) + # Artificially trigger update. + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + # Collect events. + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 3 + + state = hass.states.get("geo_location.title_1") + assert state is not None + assert state.name == "Title 1" + assert state.attributes == { + ATTR_EXTERNAL_ID: "1234", + ATTR_LATITUDE: 38.0, + ATTR_LONGITUDE: -3.0, + ATTR_FRIENDLY_NAME: "Title 1", + ATTR_LOCALITY: "Locality 1", + ATTR_ATTRIBUTION: "Attribution 1", + ATTR_TIME: datetime.datetime( + 2018, 9, 22, 8, 0, tzinfo=datetime.timezone.utc + ), + ATTR_MAGNITUDE: 5.7, + ATTR_DEPTH: 10.5, + ATTR_MMI: 5, + ATTR_QUALITY: "best", + ATTR_UNIT_OF_MEASUREMENT: "km", + ATTR_SOURCE: "geonetnz_quakes", + ATTR_ICON: "mdi:pulse", + } + assert float(state.state) == 15.5 + + state = hass.states.get("geo_location.title_2") + assert state is not None + assert state.name == "Title 2" + assert state.attributes == { + ATTR_EXTERNAL_ID: "2345", + ATTR_LATITUDE: 38.1, + ATTR_LONGITUDE: -3.1, + ATTR_FRIENDLY_NAME: "Title 2", + ATTR_MAGNITUDE: 4.6, + ATTR_UNIT_OF_MEASUREMENT: "km", + ATTR_SOURCE: "geonetnz_quakes", + ATTR_ICON: "mdi:pulse", + } + assert float(state.state) == 20.5 + + state = hass.states.get("geo_location.title_3") + assert state is not None + assert state.name == "Title 3" + assert state.attributes == { + ATTR_EXTERNAL_ID: "3456", + ATTR_LATITUDE: 38.2, + ATTR_LONGITUDE: -3.2, + ATTR_FRIENDLY_NAME: "Title 3", + ATTR_LOCALITY: "Locality 3", + ATTR_UNIT_OF_MEASUREMENT: "km", + ATTR_SOURCE: "geonetnz_quakes", + ATTR_ICON: "mdi:pulse", + } + assert float(state.state) == 25.5 + + # Simulate an update - one existing, one new entry, + # one outdated entry + mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_4, mock_entry_3] + async_fire_time_changed(hass, utcnow + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 3 + + # Simulate an update - empty data, but successful update, + # so no changes to entities. + mock_feed_update.return_value = "OK_NO_DATA", None + async_fire_time_changed(hass, utcnow + 2 * DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 3 + + # Simulate an update - empty data, removes all entities + mock_feed_update.return_value = "ERROR", None + async_fire_time_changed(hass, utcnow + 3 * DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 0 + + +async def test_setup_imperial(hass): + """Test the setup of the integration using imperial unit system.""" + hass.config.units = IMPERIAL_SYSTEM + # Set up some mock feed entries for this test. + mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 15.5, (38.0, -3.0)) + + # Patching 'utcnow' to gain more control over the timed update. + utcnow = dt_util.utcnow() + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=CoroutineMock + ) as mock_feed_update, patch( + "aio_geojson_client.feed.GeoJsonFeed.__init__", new_callable=CoroutineMock + ) as mock_feed_init: + mock_feed_update.return_value = "OK", [mock_entry_1] + assert await async_setup_component(hass, geonetnz_quakes.DOMAIN, CONFIG) + # Artificially trigger update. + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + # Collect events. + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 1 + + # Test conversion of 200 miles to kilometers. + assert mock_feed_init.call_args[1].get("filter_radius") == 321.8688 + + state = hass.states.get("geo_location.title_1") + assert state is not None + assert state.name == "Title 1" + assert state.attributes == { + ATTR_EXTERNAL_ID: "1234", + ATTR_LATITUDE: 38.0, + ATTR_LONGITUDE: -3.0, + ATTR_FRIENDLY_NAME: "Title 1", + ATTR_UNIT_OF_MEASUREMENT: "mi", + ATTR_SOURCE: "geonetnz_quakes", + ATTR_ICON: "mdi:pulse", + } + assert float(state.state) == 9.6 From 2e4905981e11445c327aef19b920905027235127 Mon Sep 17 00:00:00 2001 From: Robert Dunmire III Date: Sat, 10 Aug 2019 07:53:20 -0400 Subject: [PATCH 100/273] Fix device re-connect when API connection lost (#25842) --- homeassistant/components/mikrotik/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index cbab4812322..236892a98b9 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -172,7 +172,8 @@ class MikrotikClient: def command(self, cmd, params=None): """Retrieve data from Mikrotik API.""" if not self._connected or not self._client: - return None + if not self.connect_to_device(): + return None try: if params: response = self._client(cmd=cmd, **params) From e9705af055a3d5882a62f6059910c21fb4c2c2f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Osb=C3=A4ck?= Date: Sat, 10 Aug 2019 14:35:04 +0200 Subject: [PATCH 101/273] Prometheus metrics naming based on device_class and unit_of_measurement (#24103) * - Change how we extract the metrics for sensors - Add component filtering as seen in influxdb - Add metric override as seen in influxdb - Add more unit tests with actual device data * Extract sensor metric logic to separate handlers * Update prometheus dependency * Format using black * Format using black * Fix flake8 * Move sensor metric handler list to init * Use f strings instead of .format --- .../components/prometheus/__init__.py | 146 +++++++++++++++--- .../components/prometheus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/prometheus/test_init.py | 94 +++++++++-- 5 files changed, 207 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 80b68e52885..1ba2c4809b6 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -10,13 +10,16 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, + ATTR_DEVICE_CLASS, CONTENT_TYPE_TEXT_PLAIN, EVENT_STATE_CHANGED, TEMP_FAHRENHEIT, + TEMP_CELSIUS, ) from homeassistant.helpers import entityfilter, state as state_helper import homeassistant.helpers.config_validation as cv from homeassistant.util.temperature import fahrenheit_to_celsius +from homeassistant.helpers.entity_values import EntityValues _LOGGER = logging.getLogger(__name__) @@ -25,6 +28,14 @@ API_ENDPOINT = "/api/prometheus" DOMAIN = "prometheus" CONF_FILTER = "filter" CONF_PROM_NAMESPACE = "namespace" +CONF_COMPONENT_CONFIG = "component_config" +CONF_COMPONENT_CONFIG_GLOB = "component_config_glob" +CONF_COMPONENT_CONFIG_DOMAIN = "component_config_domain" +CONF_DEFAULT_METRIC = "default_metric" +CONF_OVERRIDE_METRIC = "override_metric" +COMPONENT_CONFIG_SCHEMA_ENTRY = vol.Schema( + {vol.Optional(CONF_OVERRIDE_METRIC): cv.string} +) CONFIG_SCHEMA = vol.Schema( { @@ -32,6 +43,17 @@ CONFIG_SCHEMA = vol.Schema( { vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA, vol.Optional(CONF_PROM_NAMESPACE): cv.string, + vol.Optional(CONF_DEFAULT_METRIC): cv.string, + vol.Optional(CONF_OVERRIDE_METRIC): cv.string, + vol.Optional(CONF_COMPONENT_CONFIG, default={}): vol.Schema( + {cv.entity_id: COMPONENT_CONFIG_SCHEMA_ENTRY} + ), + vol.Optional(CONF_COMPONENT_CONFIG_GLOB, default={}): vol.Schema( + {cv.string: COMPONENT_CONFIG_SCHEMA_ENTRY} + ), + vol.Optional(CONF_COMPONENT_CONFIG_DOMAIN, default={}): vol.Schema( + {cv.string: COMPONENT_CONFIG_SCHEMA_ENTRY} + ), } ) }, @@ -49,8 +71,22 @@ def setup(hass, config): entity_filter = conf[CONF_FILTER] namespace = conf.get(CONF_PROM_NAMESPACE) climate_units = hass.config.units.temperature_unit + override_metric = conf.get(CONF_OVERRIDE_METRIC) + default_metric = conf.get(CONF_DEFAULT_METRIC) + component_config = EntityValues( + conf[CONF_COMPONENT_CONFIG], + conf[CONF_COMPONENT_CONFIG_DOMAIN], + conf[CONF_COMPONENT_CONFIG_GLOB], + ) + metrics = PrometheusMetrics( - prometheus_client, entity_filter, namespace, climate_units + prometheus_client, + entity_filter, + namespace, + climate_units, + component_config, + override_metric, + default_metric, ) hass.bus.listen(EVENT_STATE_CHANGED, metrics.handle_event) @@ -60,12 +96,32 @@ def setup(hass, config): class PrometheusMetrics: """Model all of the metrics which should be exposed to Prometheus.""" - def __init__(self, prometheus_client, entity_filter, namespace, climate_units): + def __init__( + self, + prometheus_client, + entity_filter, + namespace, + climate_units, + component_config, + override_metric, + default_metric, + ): """Initialize Prometheus Metrics.""" self.prometheus_client = prometheus_client + self._component_config = component_config + self._override_metric = override_metric + self._default_metric = default_metric self._filter = entity_filter + self._sensor_metric_handlers = [ + self._sensor_override_component_metric, + self._sensor_override_metric, + self._sensor_attribute_metric, + self._sensor_default_metric, + self._sensor_fallback_metric, + ] + if namespace: - self.metrics_prefix = "{}_".format(namespace) + self.metrics_prefix = f"{namespace}_" else: self.metrics_prefix = "" self._metrics = {} @@ -84,7 +140,7 @@ class PrometheusMetrics: if not self._filter(state.entity_id): return - handler = "_handle_{}".format(domain) + handler = f"_handle_{domain}" if hasattr(self, handler): getattr(self, handler)(state) @@ -103,7 +159,7 @@ class PrometheusMetrics: try: return self._metrics[metric] except KeyError: - full_metric_name = "{}{}".format(self.metrics_prefix, metric) + full_metric_name = f"{self.metrics_prefix}{metric}" self._metrics[metric] = factory(full_metric_name, documentation, labels) return self._metrics[metric] @@ -229,31 +285,73 @@ class PrometheusMetrics: pass def _handle_sensor(self, state): + unit = self._unit_string(state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)) - unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - metric = state.entity_id.split(".")[1] + for metric_handler in self._sensor_metric_handlers: + metric = metric_handler(state, unit) + if metric is not None: + break - if "_" not in str(metric): - metric = state.entity_id.replace(".", "_") + if metric is not None: + _metric = self._metric( + metric, self.prometheus_client.Gauge, f"Sensor data measured in {unit}" + ) - try: - int(metric.split("_")[-1]) - metric = "_".join(metric.split("_")[:-1]) - except ValueError: - pass - - _metric = self._metric(metric, self.prometheus_client.Gauge, state.entity_id) - - try: - value = self.state_as_number(state) - if unit == TEMP_FAHRENHEIT: - value = fahrenheit_to_celsius(value) - _metric.labels(**self._labels(state)).set(value) - except ValueError: - pass + try: + value = self.state_as_number(state) + if unit == TEMP_FAHRENHEIT: + value = fahrenheit_to_celsius(value) + _metric.labels(**self._labels(state)).set(value) + except ValueError: + pass self._battery(state) + def _sensor_default_metric(self, state, unit): + """Get default metric.""" + return self._default_metric + + @staticmethod + def _sensor_attribute_metric(state, unit): + """Get metric based on device class attribute.""" + metric = state.attributes.get(ATTR_DEVICE_CLASS) + if metric is not None: + return f"{metric}_{unit}" + return None + + def _sensor_override_metric(self, state, unit): + """Get metric from override in configuration.""" + if self._override_metric: + return self._override_metric + return None + + def _sensor_override_component_metric(self, state, unit): + """Get metric from override in component confioguration.""" + return self._component_config.get(state.entity_id).get(CONF_OVERRIDE_METRIC) + + @staticmethod + def _sensor_fallback_metric(state, unit): + """Get metric from fallback logic for compatability.""" + if unit in (None, ""): + _LOGGER.debug("Unsupported sensor: %s", state.entity_id) + return None + return f"sensor_unit_{unit}" + + @staticmethod + def _unit_string(unit): + """Get a formatted string of the unit.""" + if unit is None: + return + + units = { + TEMP_CELSIUS: "c", + TEMP_FAHRENHEIT: "c", # F should go into C metric + "%": "percent", + } + default = unit.replace("/", "_per_") + default = default.lower() + return units.get(unit, default) + def _handle_switch(self, state): metric = self._metric( "switch_state", self.prometheus_client.Gauge, "State of the switch (0/1)" diff --git a/homeassistant/components/prometheus/manifest.json b/homeassistant/components/prometheus/manifest.json index d9699be6bf7..cab1228aa56 100644 --- a/homeassistant/components/prometheus/manifest.json +++ b/homeassistant/components/prometheus/manifest.json @@ -3,7 +3,7 @@ "name": "Prometheus", "documentation": "https://www.home-assistant.io/components/prometheus", "requirements": [ - "prometheus_client==0.2.0" + "prometheus_client==0.7.1" ], "dependencies": [ "http" diff --git a/requirements_all.txt b/requirements_all.txt index 66d6cba8eba..e56077423ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -965,7 +965,7 @@ prezzibenzina-py==1.1.4 proliphix==0.4.1 # homeassistant.components.prometheus -prometheus_client==0.2.0 +prometheus_client==0.7.1 # homeassistant.components.tensorflow protobuf==3.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb63f138d36..ab6c1bde776 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -244,7 +244,7 @@ pilight==0.1.1 pmsensor==0.4 # homeassistant.components.prometheus -prometheus_client==0.2.0 +prometheus_client==0.7.1 # homeassistant.components.ptvsd ptvsd==4.2.8 diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index e9f92b2d6f6..9e313fd3694 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -2,17 +2,46 @@ import asyncio import pytest +from homeassistant.const import ENERGY_KILO_WATT_HOUR, DEVICE_CLASS_POWER + +from homeassistant import setup +from homeassistant.components import climate, sensor +from homeassistant.components.demo.sensor import DemoSensor from homeassistant.setup import async_setup_component import homeassistant.components.prometheus as prometheus @pytest.fixture -def prometheus_client(loop, hass, hass_client): +async def prometheus_client(loop, hass, hass_client): """Initialize an hass_client with Prometheus component.""" - assert loop.run_until_complete( - async_setup_component(hass, prometheus.DOMAIN, {prometheus.DOMAIN: {}}) + await async_setup_component(hass, prometheus.DOMAIN, {prometheus.DOMAIN: {}}) + + await setup.async_setup_component( + hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]} ) - return loop.run_until_complete(hass_client()) + + await setup.async_setup_component( + hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]} + ) + + sensor1 = DemoSensor("Television Energy", 74, None, ENERGY_KILO_WATT_HOUR, None) + sensor1.hass = hass + sensor1.entity_id = "sensor.television_energy" + await sensor1.async_update_ha_state() + + sensor2 = DemoSensor( + "Radio Energy", 14, DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, None + ) + sensor2.hass = hass + sensor2.entity_id = "sensor.radio_energy" + await sensor2.async_update_ha_state() + + sensor3 = DemoSensor("Electricity price", 0.123, None, "SEK/kWh", None) + sensor3.hass = hass + sensor3.entity_id = "sensor.electricity_price" + await sensor3.async_update_ha_state() + + return await hass_client() @asyncio.coroutine @@ -25,11 +54,52 @@ def test_view(prometheus_client): # pylint: disable=redefined-outer-name body = yield from resp.text() body = body.split("\n") - assert len(body) > 3 # At least two comment lines and a metric - for line in body: - if line: - assert ( - line.startswith("# ") - or line.startswith("process_") - or line.startswith("python_info") - ) + assert len(body) > 3 + + assert "# HELP python_info Python platform information" in body + assert ( + "# HELP python_gc_objects_collected_total " + "Objects collected during gc" in body + ) + + assert ( + 'temperature_c{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 15.6' in body + ) + + assert ( + 'battery_level_percent{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 12.0' in body + ) + + assert ( + 'current_temperature_c{domain="climate",' + 'entity="climate.heatpump",' + 'friendly_name="HeatPump"} 25.0' in body + ) + + assert ( + 'humidity_percent{domain="sensor",' + 'entity="sensor.outside_humidity",' + 'friendly_name="Outside Humidity"} 54.0' in body + ) + + assert ( + 'sensor_unit_kwh{domain="sensor",' + 'entity="sensor.television_energy",' + 'friendly_name="Television Energy"} 74.0' in body + ) + + assert ( + 'power_kwh{domain="sensor",' + 'entity="sensor.radio_energy",' + 'friendly_name="Radio Energy"} 14.0' in body + ) + + assert ( + 'sensor_unit_sek_per_kwh{domain="sensor",' + 'entity="sensor.electricity_price",' + 'friendly_name="Electricity price"} 0.123' in body + ) From d89e8ead61344c3e3c0420c1174e0e1077ba0410 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sat, 10 Aug 2019 15:49:29 +0200 Subject: [PATCH 102/273] Fix Netatmo climate issue (#25830) * Bump pyatmo to v2.2.1 * Fix issue 25778 --- homeassistant/components/netatmo/climate.py | 4 ++-- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 109f12a87fc..9656d4a37a4 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -109,8 +109,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): auth = hass.data[DATA_NETATMO_AUTH] + home_data = HomeData(auth) try: - home_data = HomeData(auth) + home_data.setup() except pyatmo.NoDevice: return @@ -352,7 +353,6 @@ class HomeData: def get_home_ids(self): """Get all the home ids returned by NetAtmo API.""" - self.setup() if self.homedata is None: return [] for home_id in self.homedata.homes: diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 66b0efc61ff..82f32c34407 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==2.2.0" + "pyatmo==2.2.1" ], "dependencies": [ "webhook" diff --git a/requirements_all.txt b/requirements_all.txt index e56077423ef..ae339702c31 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1053,7 +1053,7 @@ pyalarmdotcom==0.3.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==2.2.0 +pyatmo==2.2.1 # homeassistant.components.apple_tv pyatv==0.3.12 From e7e083c547fdd7a125dbcc14b92376bbeef3be7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Lov=C3=A9n?= Date: Sat, 10 Aug 2019 21:46:49 +0200 Subject: [PATCH 103/273] Websocket call for rendering jinja2 templates subscription (#25614) * Websocket call for rendering jinja2 templates * Address review comments * Address review comments * Allow MATCH_ALL, but ignore it. * Always register unsub method. --- .../components/websocket_api/commands.py | 44 ++++++++++ .../components/websocket_api/test_commands.py | 82 +++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 0dc07ebfd3f..deb3600574f 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -7,6 +7,7 @@ from homeassistant.core import callback, DOMAIN as HASS_DOMAIN from homeassistant.exceptions import Unauthorized, ServiceNotFound, HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_get_all_descriptions +from homeassistant.helpers.event import async_track_state_change from . import const, decorators, messages @@ -21,6 +22,7 @@ def async_register_commands(hass, async_reg): async_reg(hass, handle_get_services) async_reg(hass, handle_get_config) async_reg(hass, handle_ping) + async_reg(hass, handle_render_template) def pong_message(iden): @@ -202,3 +204,45 @@ def handle_ping(hass, connection, msg): Async friendly. """ connection.send_message(pong_message(msg["id"])) + + +@callback +@decorators.websocket_command( + { + vol.Required("type"): "render_template", + vol.Required("template"): cv.template, + vol.Optional("entity_ids"): cv.entity_ids, + vol.Optional("variables"): dict, + } +) +def handle_render_template(hass, connection, msg): + """Handle render_template command. + + Async friendly. + """ + template = msg["template"] + template.hass = hass + + variables = msg.get("variables") + + entity_ids = msg.get("entity_ids") + if entity_ids is None: + entity_ids = template.extract_entities(variables) + + @callback + def state_listener(*_): + connection.send_message( + messages.event_message( + msg["id"], {"result": template.async_render(variables)} + ) + ) + + if entity_ids and entity_ids != MATCH_ALL: + connection.subscriptions[msg["id"]] = async_track_state_change( + hass, entity_ids, state_listener + ) + else: + connection.subscriptions[msg["id"]] = lambda: None + + connection.send_result(msg["id"]) + state_listener() diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 506a45694c0..a39a0a0e7a6 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -389,3 +389,85 @@ async def test_subscribe_unsubscribe_events_state_changed( assert msg["type"] == "event" assert msg["event"]["event_type"] == "state_changed" assert msg["event"]["data"]["entity_id"] == "light.permitted" + + +async def test_render_template_renders_template( + hass, websocket_client, hass_admin_user +): + """Test simple template is rendered and updated.""" + hass.states.async_set("light.test", "on") + + await websocket_client.send_json( + { + "id": 5, + "type": "render_template", + "template": "State is: {{ states('light.test') }}", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + event = msg["event"] + assert event == {"result": "State is: on"} + + hass.states.async_set("light.test", "off") + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + event = msg["event"] + assert event == {"result": "State is: off"} + + +async def test_render_template_with_manual_entity_ids( + hass, websocket_client, hass_admin_user +): + """Test that updates to specified entity ids cause a template rerender.""" + hass.states.async_set("light.test", "on") + hass.states.async_set("light.test2", "on") + + await websocket_client.send_json( + { + "id": 5, + "type": "render_template", + "template": "State is: {{ states('light.test') }}", + "entity_ids": ["light.test2"], + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + event = msg["event"] + assert event == {"result": "State is: on"} + + hass.states.async_set("light.test2", "off") + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + event = msg["event"] + assert event == {"result": "State is: on"} + + +async def test_render_template_returns_with_match_all( + hass, websocket_client, hass_admin_user +): + """Test that a template that would match with all entities still return success.""" + await websocket_client.send_json( + {"id": 5, "type": "render_template", "template": "State is: {{ 42 }}"} + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] From 9041201c76ce63c76178307fcee56ee0244f4853 Mon Sep 17 00:00:00 2001 From: tombbo <53979375+tombbo@users.noreply.github.com> Date: Sat, 10 Aug 2019 22:24:03 +0200 Subject: [PATCH 104/273] Fix KNX Climate mode change callback (#25851) - fix KNX Climate not updating UI after receiving mode change telegram from KNX bus --- homeassistant/components/knx/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index a3183c3b34d..07aac11b972 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -209,6 +209,7 @@ class KNXClimate(ClimateDevice): await self.async_update_ha_state() self.device.register_device_updated_cb(after_update_callback) + self.device.mode.register_device_updated_cb(after_update_callback) @property def name(self) -> str: From ff92307d65aa30e2b964acb3f2aa7ea255becdc4 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sat, 10 Aug 2019 13:25:03 -0700 Subject: [PATCH 105/273] Bump androidtv to 0.0.22 (#25848) --- homeassistant/components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 1820c5123e4..ec26cf44550 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,7 +3,7 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/components/androidtv", "requirements": [ - "androidtv==0.0.21" + "androidtv==0.0.22" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/requirements_all.txt b/requirements_all.txt index ae339702c31..d44e5afe2b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -194,7 +194,7 @@ ambiclimate==0.2.0 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.21 +androidtv==0.0.22 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 From 68ee8286741f970feefdbf5c9b874bd54c6973f0 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sat, 10 Aug 2019 14:31:04 -0700 Subject: [PATCH 106/273] Move Kodi services from 'media_player' domain to 'kodi' (#25753) * Create const.py * Register services to 'kodi' domain, not 'media_player' * Add const.py to .coveragerc * 'DATA_KODI' -> 'DOMAIN' * Move the Kodi services descriptions to the Kodi component * Register Kodi services in __init__.py * Finish registering Kodi services in __init__.py * Remove logging statement intended only for testing * Combine homeassistant.const imports * Add __init__.py to .coveragerc --- .coveragerc | 2 + homeassistant/components/kodi/__init__.py | 90 ++++++++++++++++++ homeassistant/components/kodi/const.py | 2 + homeassistant/components/kodi/media_player.py | 91 ++----------------- homeassistant/components/kodi/services.yaml | 30 ++++++ .../components/media_player/services.yaml | 29 ------ 6 files changed, 131 insertions(+), 113 deletions(-) create mode 100644 homeassistant/components/kodi/const.py diff --git a/.coveragerc b/.coveragerc index 24844630f4a..445d3e163ee 100644 --- a/.coveragerc +++ b/.coveragerc @@ -317,6 +317,8 @@ omit = homeassistant/components/knx/* homeassistant/components/knx/climate.py homeassistant/components/knx/cover.py + homeassistant/components/kodi/__init__.py + homeassistant/components/kodi/const.py homeassistant/components/kodi/media_player.py homeassistant/components/kodi/notify.py homeassistant/components/konnected/* diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py index cbe20384103..5bbffc5df1d 100644 --- a/homeassistant/components/kodi/__init__.py +++ b/homeassistant/components/kodi/__init__.py @@ -1 +1,91 @@ """The kodi component.""" + +import asyncio +import voluptuous as vol + +from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM +from homeassistant.helpers import config_validation as cv +from homeassistant.components.kodi.const import DOMAIN +from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN + + +SERVICE_ADD_MEDIA = "add_to_playlist" +SERVICE_CALL_METHOD = "call_method" + +ATTR_MEDIA_TYPE = "media_type" +ATTR_MEDIA_NAME = "media_name" +ATTR_MEDIA_ARTIST_NAME = "artist_name" +ATTR_MEDIA_ID = "media_id" +ATTR_METHOD = "method" + +MEDIA_PLAYER_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.comp_entity_ids}) + +KODI_ADD_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend( + { + vol.Required(ATTR_MEDIA_TYPE): cv.string, + vol.Optional(ATTR_MEDIA_ID): cv.string, + vol.Optional(ATTR_MEDIA_NAME): cv.string, + vol.Optional(ATTR_MEDIA_ARTIST_NAME): cv.string, + } +) +KODI_CALL_METHOD_SCHEMA = MEDIA_PLAYER_SCHEMA.extend( + {vol.Required(ATTR_METHOD): cv.string}, extra=vol.ALLOW_EXTRA +) + +SERVICE_TO_METHOD = { + SERVICE_ADD_MEDIA: { + "method": "async_add_media_to_playlist", + "schema": KODI_ADD_MEDIA_SCHEMA, + }, + SERVICE_CALL_METHOD: { + "method": "async_call_method", + "schema": KODI_CALL_METHOD_SCHEMA, + }, +} + + +async def async_setup(hass, config): + """Set up the Kodi integration.""" + if any( + ((CONF_PLATFORM, DOMAIN) in cfg.items() for cfg in config.get(MP_DOMAIN, [])) + ): + # Register the Kodi media_player services + async def async_service_handler(service): + """Map services to methods on MediaPlayerDevice.""" + method = SERVICE_TO_METHOD.get(service.service) + if not method: + return + + params = { + key: value for key, value in service.data.items() if key != "entity_id" + } + entity_ids = service.data.get("entity_id") + if entity_ids: + target_players = [ + player + for player in hass.data[DOMAIN].values() + if player.entity_id in entity_ids + ] + else: + target_players = hass.data[DOMAIN].values() + + update_tasks = [] + for player in target_players: + await getattr(player, method["method"])(**params) + + for player in target_players: + if player.should_poll: + update_coro = player.async_update_ha_state(True) + update_tasks.append(update_coro) + + if update_tasks: + await asyncio.wait(update_tasks) + + for service in SERVICE_TO_METHOD: + schema = SERVICE_TO_METHOD[service]["schema"] + hass.services.async_register( + DOMAIN, service, async_service_handler, schema=schema + ) + + # Return boolean to indicate that initialization was successful. + return True diff --git a/homeassistant/components/kodi/const.py b/homeassistant/components/kodi/const.py new file mode 100644 index 00000000000..7cb93f0d283 --- /dev/null +++ b/homeassistant/components/kodi/const.py @@ -0,0 +1,2 @@ +"""Constants for the Kodi platform.""" +DOMAIN = "kodi" diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index de50a2ef4de..14ef0292ecc 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -1,5 +1,4 @@ """Support for interfacing with the XBMC/Kodi JSON-RPC API.""" -import asyncio from collections import OrderedDict from functools import wraps import logging @@ -10,9 +9,10 @@ import urllib import aiohttp import voluptuous as vol +from homeassistant.components.kodi import SERVICE_CALL_METHOD +from homeassistant.components.kodi.const import DOMAIN from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - DOMAIN, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, @@ -34,7 +34,6 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PASSWORD, @@ -134,42 +133,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -SERVICE_ADD_MEDIA = "kodi_add_to_playlist" -SERVICE_CALL_METHOD = "kodi_call_method" - -DATA_KODI = "kodi" - -ATTR_MEDIA_TYPE = "media_type" -ATTR_MEDIA_NAME = "media_name" -ATTR_MEDIA_ARTIST_NAME = "artist_name" -ATTR_MEDIA_ID = "media_id" -ATTR_METHOD = "method" - -MEDIA_PLAYER_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.comp_entity_ids}) - -MEDIA_PLAYER_ADD_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend( - { - vol.Required(ATTR_MEDIA_TYPE): cv.string, - vol.Optional(ATTR_MEDIA_ID): cv.string, - vol.Optional(ATTR_MEDIA_NAME): cv.string, - vol.Optional(ATTR_MEDIA_ARTIST_NAME): cv.string, - } -) -MEDIA_PLAYER_CALL_METHOD_SCHEMA = MEDIA_PLAYER_SCHEMA.extend( - {vol.Required(ATTR_METHOD): cv.string}, extra=vol.ALLOW_EXTRA -) - -SERVICE_TO_METHOD = { - SERVICE_ADD_MEDIA: { - "method": "async_add_media_to_playlist", - "schema": MEDIA_PLAYER_ADD_MEDIA_SCHEMA, - }, - SERVICE_CALL_METHOD: { - "method": "async_call_method", - "schema": MEDIA_PLAYER_CALL_METHOD_SCHEMA, - }, -} - def _check_deprecated_turn_off(hass, turn_off_action): """Create an equivalent script for old turn off actions.""" @@ -205,8 +168,8 @@ def _check_deprecated_turn_off(hass, turn_off_action): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Kodi platform.""" - if DATA_KODI not in hass.data: - hass.data[DATA_KODI] = dict() + if DOMAIN not in hass.data: + hass.data[DOMAIN] = dict() unique_id = None # Is this a manual configuration? @@ -231,14 +194,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Only add a device once, so discovered devices do not override manual # config. ip_addr = socket.gethostbyname(host) - if ip_addr in hass.data[DATA_KODI]: + if ip_addr in hass.data[DOMAIN]: return # If we got an unique id, check that it does not exist already. # This is necessary as netdisco does not deterministally return the same # advertisement when the service is offered over multiple IP addresses. if unique_id is not None: - for device in hass.data[DATA_KODI].values(): + for device in hass.data[DOMAIN].values(): if device.unique_id == unique_id: return @@ -258,49 +221,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= unique_id=unique_id, ) - hass.data[DATA_KODI][ip_addr] = entity + hass.data[DOMAIN][ip_addr] = entity async_add_entities([entity], update_before_add=True) - async def async_service_handler(service): - """Map services to methods on MediaPlayerDevice.""" - method = SERVICE_TO_METHOD.get(service.service) - if not method: - return - - params = { - key: value for key, value in service.data.items() if key != "entity_id" - } - entity_ids = service.data.get("entity_id") - if entity_ids: - target_players = [ - player - for player in hass.data[DATA_KODI].values() - if player.entity_id in entity_ids - ] - else: - target_players = hass.data[DATA_KODI].values() - - update_tasks = [] - for player in target_players: - await getattr(player, method["method"])(**params) - - for player in target_players: - if player.should_poll: - update_coro = player.async_update_ha_state(True) - update_tasks.append(update_coro) - - if update_tasks: - await asyncio.wait(update_tasks) - - if hass.services.has_service(DOMAIN, SERVICE_ADD_MEDIA): - return - - for service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[service]["schema"] - hass.services.async_register( - DOMAIN, service, async_service_handler, schema=schema - ) - def cmd(func): """Catch command exceptions.""" diff --git a/homeassistant/components/kodi/services.yaml b/homeassistant/components/kodi/services.yaml index e69de29bb2d..01dde6a249c 100644 --- a/homeassistant/components/kodi/services.yaml +++ b/homeassistant/components/kodi/services.yaml @@ -0,0 +1,30 @@ +# Describes the format for available Kodi services + +add_to_playlist: + description: Add music to the default playlist (i.e. playlistid=0). + fields: + entity_id: + description: Name(s) of the Kodi entities where to add the media. + example: 'media_player.living_room_kodi' + media_type: + description: Media type identifier. It must be one of SONG or ALBUM. + example: ALBUM + media_id: + description: Unique Id of the media entry to add (`songid` or albumid`). If not defined, `media_name` and `artist_name` are needed to search the Kodi music library. + example: 123456 + media_name: + description: Optional media name for filtering media. Can be 'ALL' when `media_type` is 'ALBUM' and `artist_name` is specified, to add all songs from one artist. + example: 'Highway to Hell' + artist_name: + description: Optional artist name for filtering media. + example: 'AC/DC' + +call_method: + description: 'Call a Kodi JSONRPC API method with optional parameters. Results of the Kodi API call will be redirected in a Home Assistant event: `kodi_call_method_result`.' + fields: + entity_id: + description: Name(s) of the Kodi entities where to run the API method. + example: 'media_player.living_room_kodi' + method: + description: Name of the Kodi JSONRPC API method to be called. + example: 'VideoLibrary.GetRecentlyAddedEpisodes' diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index d7f636d070a..5421085c308 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -232,35 +232,6 @@ soundtouch_remove_zone_slave: description: Name of slaves entities to remove from the existing zone. example: 'media_player.soundtouch_bedroom' -kodi_add_to_playlist: - description: Add music to the default playlist (i.e. playlistid=0). - fields: - entity_id: - description: Name(s) of the Kodi entities where to add the media. - example: 'media_player.living_room_kodi' - media_type: - description: Media type identifier. It must be one of SONG or ALBUM. - example: ALBUM - media_id: - description: Unique Id of the media entry to add (`songid` or albumid`). If not defined, `media_name` and `artist_name` are needed to search the Kodi music library. - example: 123456 - media_name: - description: Optional media name for filtering media. Can be 'ALL' when `media_type` is 'ALBUM' and `artist_name` is specified, to add all songs from one artist. - example: 'Highway to Hell' - artist_name: - description: Optional artist name for filtering media. - example: 'AC/DC' - -kodi_call_method: - description: 'Call a Kodi JSONRPC API method with optional parameters. Results of the Kodi API call will be redirected in a Home Assistant event: `kodi_call_method_result`.' - fields: - entity_id: - description: Name(s) of the Kodi entities where to run the API method. - example: 'media_player.living_room_kodi' - method: - description: Name of the Kodi JSONRPC API method to be called. - example: 'VideoLibrary.GetRecentlyAddedEpisodes' - squeezebox_call_method: description: 'Call a Squeezebox JSON/RPC API method.' fields: From a6f1773492f499dfe60fdb8ef20939650a99ced1 Mon Sep 17 00:00:00 2001 From: MatsNl <37705266+MatsNl@users.noreply.github.com> Date: Sat, 10 Aug 2019 23:45:47 +0200 Subject: [PATCH 107/273] Update sensor.py (#25825) exclude "unknown" values from measurements --- homeassistant/components/statistics/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 30e112fdfbf..252a29591c9 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_ENTITY_ID, EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, + STATE_UNAVAILABLE, ATTR_UNIT_OF_MEASUREMENT, ) from homeassistant.core import callback @@ -131,7 +132,7 @@ class StatisticsSensor(Entity): def _add_state_to_queue(self, new_state): """Add the state to the queue.""" - if new_state.state == STATE_UNKNOWN: + if new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]: return try: From 3648db721426971d2a811968720887b06f32031e Mon Sep 17 00:00:00 2001 From: Oncleben31 Date: Sat, 10 Aug 2019 23:55:58 +0200 Subject: [PATCH 108/273] Add pip-wheel-metadata in .gitignore (#25832) When developing in a container with VSC, the folder `pip-wheel-metadata` is created during the container building process. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ff3e8d838a3..65b325a0a4b 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ develop-eggs .installed.cfg lib lib64 +pip-wheel-metadata # Logs *.log From ce0edf8360bf8a694df474aea4c9d01c17f618a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20P=C3=A9rez?= Date: Sun, 11 Aug 2019 00:01:19 +0200 Subject: [PATCH 109/273] Fix configuration field name (#25840) --- homeassistant/components/plex/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 2579a004e88..98137897149 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -262,7 +262,7 @@ def request_configuration(host, hass, config, add_entities_callback): host, data.get("token"), cv.boolean(data.get("has_ssl")), - cv.boolean(data.get("do_not_verify")), + cv.boolean(data.get("do_not_verify_ssl")), hass, config, add_entities_callback, From 8b9d0593b1875d2cf444fd4a494f3e8c16ed59e5 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Sun, 11 Aug 2019 01:30:33 +0200 Subject: [PATCH 110/273] Allow components with empty list config (i.e. person) in packages (#25827) * Fix #23424 * mypy Lists --- homeassistant/config.py | 28 +++++++++------------- homeassistant/helpers/config_validation.py | 12 +++++----- tests/test_config.py | 5 ++-- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 4d3d4dd841f..ccf1317e660 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -631,10 +631,9 @@ def _recursive_merge(conf: Dict[str, Any], package: Dict[str, Any]) -> Union[boo error = _recursive_merge(conf=conf[key], package=pack_conf) elif isinstance(pack_conf, list): - if not pack_conf: - continue - conf[key] = cv.ensure_list(conf.get(key)) - conf[key].extend(cv.ensure_list(pack_conf)) + conf[key] = cv.remove_falsy( + cv.ensure_list(conf.get(key)) + cv.ensure_list(pack_conf) + ) else: if conf.get(key) is not None: @@ -669,22 +668,17 @@ async def merge_packages_config( _log_pkg_error(pack_name, comp_name, config, str(ex)) continue - if hasattr(component, "PLATFORM_SCHEMA"): - if not comp_conf: - continue # Ensure we dont add Falsy items to list - config[comp_name] = cv.ensure_list(config.get(comp_name)) - config[comp_name].extend(cv.ensure_list(comp_conf)) - continue + merge_list = hasattr(component, "PLATFORM_SCHEMA") - if hasattr(component, "CONFIG_SCHEMA"): + if not merge_list and hasattr(component, "CONFIG_SCHEMA"): merge_type, _ = _identify_config_schema(component) + merge_list = merge_type == "list" - if merge_type == "list": - if not comp_conf: - continue # Ensure we dont add Falsy items to list - config[comp_name] = cv.ensure_list(config.get(comp_name)) - config[comp_name].extend(cv.ensure_list(comp_conf)) - continue + if merge_list: + config[comp_name] = cv.remove_falsy( + cv.ensure_list(config.get(comp_name)) + cv.ensure_list(comp_conf) + ) + continue if comp_conf is None: comp_conf = OrderedDict() diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index b52a17b2e39..db96f4a2d02 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -11,7 +11,7 @@ from datetime import ( ) from socket import _GLOBAL_DEFAULT_TIMEOUT from numbers import Number -from typing import Any, Union, TypeVar, Callable, Sequence, Dict, Optional +from typing import Any, Union, TypeVar, Callable, List, Dict, Optional from urllib.parse import urlparse from uuid import UUID @@ -191,7 +191,7 @@ def isdir(value: Any) -> str: return dir_in -def ensure_list(value: Union[T, Sequence[T], None]) -> Sequence[T]: +def ensure_list(value: Union[T, List[T], None]) -> List[T]: """Wrap value in list if it is not one.""" if value is None: return [] @@ -207,7 +207,7 @@ def entity_id(value: Any) -> str: raise vol.Invalid("Entity ID {} is an invalid entity id".format(value)) -def entity_ids(value: Union[str, Sequence]) -> Sequence[str]: +def entity_ids(value: Union[str, List]) -> List[str]: """Validate Entity IDs.""" if value is None: raise vol.Invalid("Entity IDs can not be None") @@ -234,7 +234,7 @@ def entity_domain(domain: str): def entities_domain(domain: str): """Validate that entities belong to domain.""" - def validate(values: Union[str, Sequence]) -> Sequence[str]: + def validate(values: Union[str, List]) -> List[str]: """Test if entity domain is domain.""" values = entity_ids(values) for ent_id in values: @@ -370,7 +370,7 @@ def positive_timedelta(value: timedelta) -> timedelta: return value -def remove_falsy(value: Sequence[T]) -> Sequence[T]: +def remove_falsy(value: List[T]) -> List[T]: """Remove falsy values from a list.""" return [v for v in value if v] @@ -562,7 +562,7 @@ def uuid4_hex(value): return result.hex -def ensure_list_csv(value: Any) -> Sequence: +def ensure_list_csv(value: Any) -> List: """Ensure that input is a list or make one from comma-separated string.""" if isinstance(value, str): return [member.strip() for member in value.split(",")] diff --git a/tests/test_config.py b/tests/test_config.py index 6d85712bc25..a67cd345797 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -735,7 +735,7 @@ async def test_merge_once_only_lists(hass): """Test if we have a merge for a comp that may occur only once. Lists.""" packages = { "pack_2": { - "api": {"list_1": ["item_2", "item_3"], "list_2": ["item_1"], "list_3": []} + "api": {"list_1": ["item_2", "item_3"], "list_2": ["item_4"], "list_3": []} } } config = { @@ -745,7 +745,8 @@ async def test_merge_once_only_lists(hass): await config_util.merge_packages_config(hass, config, packages) assert config["api"] == { "list_1": ["item_1", "item_2", "item_3"], - "list_2": ["item_1"], + "list_2": ["item_4"], + "list_3": [], } From b8460bb331cc0b1af73d9e3a8e412bc2c3f842ad Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Sat, 10 Aug 2019 23:47:36 -0500 Subject: [PATCH 111/273] Always populate hvac_modes in SmartThings climate platform (#25859) * Always return list for hvac_modes * Use climate constants --- .../components/smartthings/climate.py | 13 +++++----- tests/components/smartthings/test_climate.py | 25 ++++++++++--------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index e9fefeb2995..bb307523e97 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -228,35 +228,34 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateDevice): self._hvac_mode = MODE_TO_STATE.get(thermostat_mode) if self._hvac_mode is None: _LOGGER.debug( - "Device %s (%s) returned an invalid" "hvac mode: %s", + "Device %s (%s) returned an invalid hvac mode: %s", self._device.label, self._device.device_id, thermostat_mode, ) + modes = set() supported_modes = self._device.status.supported_thermostat_modes if isinstance(supported_modes, Iterable): - operations = set() for mode in supported_modes: state = MODE_TO_STATE.get(mode) if state is not None: - operations.add(state) + modes.add(state) else: _LOGGER.debug( - "Device %s (%s) returned an invalid " - "supported thermostat mode: %s", + "Device %s (%s) returned an invalid supported thermostat mode: %s", self._device.label, self._device.device_id, mode, ) - self._hvac_modes = operations else: _LOGGER.debug( - "Device %s (%s) returned invalid supported " "thermostat modes: %s", + "Device %s (%s) returned invalid supported thermostat modes: %s", self._device.label, self._device.device_id, supported_modes, ) + self._hvac_modes = list(modes) @property def current_humidity(self): diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 01206ded062..c366761ea1f 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -214,14 +214,14 @@ async def test_legacy_thermostat_entity_state(hass, legacy_thermostat): | SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_TARGET_TEMPERATURE ) - assert state.attributes[ATTR_HVAC_ACTIONS] == "idle" - assert state.attributes[ATTR_HVAC_MODES] == { + assert state.attributes[ATTR_HVAC_ACTIONS] == CURRENT_HVAC_IDLE + assert sorted(state.attributes[ATTR_HVAC_MODES]) == [ HVAC_MODE_AUTO, HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, - } + ] assert state.attributes[ATTR_FAN_MODE] == "auto" assert state.attributes[ATTR_FAN_MODES] == ["auto", "on"] assert state.attributes[ATTR_TARGET_TEMP_LOW] == 20 # celsius @@ -239,12 +239,12 @@ async def test_basic_thermostat_entity_state(hass, basic_thermostat): == SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_TARGET_TEMPERATURE ) assert ATTR_HVAC_ACTIONS not in state.attributes - assert state.attributes[ATTR_HVAC_MODES] == { - HVAC_MODE_OFF, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_HEAT, + assert sorted(state.attributes[ATTR_HVAC_MODES]) == [ HVAC_MODE_COOL, - } + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + ] assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 21.1 # celsius @@ -260,13 +260,13 @@ async def test_thermostat_entity_state(hass, thermostat): | SUPPORT_TARGET_TEMPERATURE ) assert state.attributes[ATTR_HVAC_ACTIONS] == CURRENT_HVAC_IDLE - assert state.attributes[ATTR_HVAC_MODES] == { + assert sorted(state.attributes[ATTR_HVAC_MODES]) == [ HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, - } + ] assert state.attributes[ATTR_FAN_MODE] == "on" assert state.attributes[ATTR_FAN_MODES] == ["auto", "on"] assert state.attributes[ATTR_TEMPERATURE] == 20 # celsius @@ -286,6 +286,7 @@ async def test_buggy_thermostat_entity_state(hass, buggy_thermostat): assert state.state is STATE_UNKNOWN assert state.attributes[ATTR_TEMPERATURE] is None assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 21.1 # celsius + assert state.attributes[ATTR_HVAC_MODES] == [] async def test_buggy_thermostat_invalid_mode(hass, buggy_thermostat): @@ -295,7 +296,7 @@ async def test_buggy_thermostat_invalid_mode(hass, buggy_thermostat): ) await setup_platform(hass, CLIMATE_DOMAIN, devices=[buggy_thermostat]) state = hass.states.get("climate.buggy_thermostat") - assert state.attributes[ATTR_HVAC_MODES] == {"heat"} + assert state.attributes[ATTR_HVAC_MODES] == [HVAC_MODE_HEAT] async def test_air_conditioner_entity_state(hass, air_conditioner): From ab7db5fbd0c7261fbd1b2e7feeceaeea0c1b4f3a Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 11 Aug 2019 22:40:44 +0200 Subject: [PATCH 112/273] UniFi - Use state to know if device is online (#25876) --- homeassistant/components/unifi/device_tracker.py | 13 ++++++++----- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifi/test_device_tracker.py | 2 ++ 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index d9f90de7888..42a6f496a2a 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -294,7 +294,7 @@ class UniFiDeviceTracker(ScannerEntity): CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME ) - if self.device.last_seen and ( + if self.device.state == 1 and ( dt_util.utcnow() - dt_util.utc_from_timestamp(float(self.device.last_seen)) < detection_time ): @@ -339,15 +339,18 @@ class UniFiDeviceTracker(ScannerEntity): @property def device_state_attributes(self): """Return the device state attributes.""" - if not self.device.last_seen: + if self.device.state == 0: return {} attributes = {} - attributes["upgradable"] = self.device.upgradable - attributes["overheating"] = self.device.overheating - if self.device.has_fan: attributes["fan_level"] = self.device.fan_level + if self.device.overheating: + attributes["overheating"] = self.device.overheating + + if self.device.upgradable: + attributes["upgradable"] = self.device.upgradable + return attributes diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index bcee022e1c4..d182806c4ac 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/unifi", "requirements": [ - "aiounifi==10" + "aiounifi==11" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index d44e5afe2b3..e513dcd6715 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -173,7 +173,7 @@ aiopvapi==1.6.14 aioswitcher==2019.4.26 # homeassistant.components.unifi -aiounifi==10 +aiounifi==11 # homeassistant.components.wwlln aiowwlln==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab6c1bde776..b873eb2e105 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -71,7 +71,7 @@ aionotion==1.1.0 aioswitcher==2019.4.26 # homeassistant.components.unifi -aiounifi==10 +aiounifi==11 # homeassistant.components.wwlln aiowwlln==1.0.0 diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 0d8d631d8ff..d5783e58818 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -69,6 +69,7 @@ DEVICE_1 = { "model": "US16P150", "name": "device_1", "overheating": False, + "state": 1, "type": "usw", "upgradable": False, "version": "4.0.42.10433", @@ -81,6 +82,7 @@ DEVICE_2 = { "mac": "00:00:00:00:01:01", "model": "US16P150", "name": "device_1", + "state": 0, "type": "usw", "version": "4.0.42.10433", } From cf90e49b50a1f333d2222ca7f9fb347ea412920f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 11 Aug 2019 20:03:21 -0700 Subject: [PATCH 113/273] Make reproduce state use platform instead of rely on function (#25856) * Make reproduce state use platform instead of rely on function * Fix types * address comment Martin. --- homeassistant/components/climate/__init__.py | 1 - .../components/climate/reproduce_state.py | 2 -- homeassistant/components/group/__init__.py | 1 - .../components/group/reproduce_state.py | 2 -- .../components/media_player/__init__.py | 1 - .../media_player/reproduce_state.py | 2 -- homeassistant/helpers/state.py | 28 ++++++++++++++----- .../climate/test_reproduce_state.py | 2 +- .../components/group/test_reproduce_state.py | 2 +- .../media_player/test_reproduce_state.py | 2 +- tests/helpers/test_state.py | 4 +-- 11 files changed, 26 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 6c64d667254..af67be5eccc 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -70,7 +70,6 @@ from .const import ( SUPPORT_TARGET_TEMPERATURE_RANGE, SUPPORT_TARGET_TEMPERATURE, ) -from .reproduce_state import async_reproduce_states # noqa DEFAULT_MIN_TEMP = 7 DEFAULT_MAX_TEMP = 35 diff --git a/homeassistant/components/climate/reproduce_state.py b/homeassistant/components/climate/reproduce_state.py index 98f085c1e8d..34e72a27c92 100644 --- a/homeassistant/components/climate/reproduce_state.py +++ b/homeassistant/components/climate/reproduce_state.py @@ -5,7 +5,6 @@ from typing import Iterable, Optional from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.loader import bind_hass from .const import ( ATTR_AUX_HEAT, @@ -69,7 +68,6 @@ async def _async_reproduce_states( await call_service(SERVICE_SET_HUMIDITY, [ATTR_HUMIDITY]) -@bind_hass async def async_reproduce_states( hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None ) -> None: diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index fc10fa2f737..75b45471982 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -34,7 +34,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.util.async_ import run_coroutine_threadsafe -from .reproduce_state import async_reproduce_states # noqa DOMAIN = "group" diff --git a/homeassistant/components/group/reproduce_state.py b/homeassistant/components/group/reproduce_state.py index f2170c4df16..827e9bb1dcb 100644 --- a/homeassistant/components/group/reproduce_state.py +++ b/homeassistant/components/group/reproduce_state.py @@ -3,10 +3,8 @@ from typing import Iterable, Optional from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.loader import bind_hass -@bind_hass async def async_reproduce_states( hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None ) -> None: diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 844f4e22089..8334577ac30 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -96,7 +96,6 @@ from .const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from .reproduce_state import async_reproduce_states # noqa _LOGGER = logging.getLogger(__name__) _RND = SystemRandom() diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index 8180a6f358b..a926274e641 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -19,7 +19,6 @@ from homeassistant.const import ( ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.loader import bind_hass from .const import ( ATTR_MEDIA_VOLUME_LEVEL, @@ -89,7 +88,6 @@ async def _async_reproduce_states( ) -@bind_hass async def async_reproduce_states( hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None ) -> None: diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index fe608ce8b6c..60aceee110f 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -4,7 +4,7 @@ import datetime as dt import json import logging from collections import defaultdict -from types import TracebackType +from types import ModuleType, TracebackType from typing import ( # noqa: F401 pylint: disable=unused-import Awaitable, Dict, @@ -16,7 +16,7 @@ from typing import ( # noqa: F401 pylint: disable=unused-import Union, ) -from homeassistant.loader import bind_hass +from homeassistant.loader import bind_hass, async_get_integration, IntegrationNotFound import homeassistant.util.dt as dt_util from homeassistant.components.notify import ATTR_MESSAGE, SERVICE_NOTIFY from homeassistant.components.sun import STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON @@ -152,13 +152,27 @@ async def async_reproduce_state( for state in states: to_call[state.domain].append(state) - async def worker(domain: str, data: List[State]) -> None: - component = getattr(hass.components, domain) - if hasattr(component, "async_reproduce_states"): - await component.async_reproduce_states(data, context=context) + async def worker(domain: str, states_by_domain: List[State]) -> None: + try: + integration = await async_get_integration(hass, domain) + except IntegrationNotFound: + _LOGGER.warning( + "Trying to reproduce state for unknown integration: %s", domain + ) + return + + try: + platform: Optional[ModuleType] = integration.get_platform("reproduce_state") + except ImportError: + platform = None + + if platform: + await platform.async_reproduce_states( # type: ignore + hass, states_by_domain, context=context + ) else: await async_reproduce_state_legacy( - hass, domain, data, blocking=blocking, context=context + hass, domain, states_by_domain, blocking=blocking, context=context ) if to_call: diff --git a/tests/components/climate/test_reproduce_state.py b/tests/components/climate/test_reproduce_state.py index 293cebdb778..fe995868840 100644 --- a/tests/components/climate/test_reproduce_state.py +++ b/tests/components/climate/test_reproduce_state.py @@ -2,7 +2,7 @@ import pytest -from homeassistant.components.climate import async_reproduce_states +from homeassistant.components.climate.reproduce_state import async_reproduce_states from homeassistant.components.climate.const import ( ATTR_AUX_HEAT, ATTR_HUMIDITY, diff --git a/tests/components/group/test_reproduce_state.py b/tests/components/group/test_reproduce_state.py index 3592f2c39ff..502ea9e51fc 100644 --- a/tests/components/group/test_reproduce_state.py +++ b/tests/components/group/test_reproduce_state.py @@ -2,7 +2,7 @@ from asyncio import Future from unittest.mock import patch -from homeassistant.components.group import async_reproduce_states +from homeassistant.components.group.reproduce_state import async_reproduce_states from homeassistant.core import Context, State diff --git a/tests/components/media_player/test_reproduce_state.py b/tests/components/media_player/test_reproduce_state.py index 3741b6e392a..ddc5d6cf0ca 100644 --- a/tests/components/media_player/test_reproduce_state.py +++ b/tests/components/media_player/test_reproduce_state.py @@ -2,7 +2,7 @@ import pytest -from homeassistant.components.media_player import async_reproduce_states +from homeassistant.components.media_player.reproduce_state import async_reproduce_states from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index a7e4df068fa..7f428c0833d 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -53,13 +53,13 @@ def test_async_track_states(hass): def test_call_to_component(hass): """Test calls to components state reproduction functions.""" with patch( - ("homeassistant.components.media_player." "async_reproduce_states") + ("homeassistant.components.media_player.reproduce_state.async_reproduce_states") ) as media_player_fun: media_player_fun.return_value = asyncio.Future() media_player_fun.return_value.set_result(None) with patch( - ("homeassistant.components.climate." "async_reproduce_states") + ("homeassistant.components.climate.reproduce_state.async_reproduce_states") ) as climate_fun: climate_fun.return_value = asyncio.Future() climate_fun.return_value.set_result(None) From b738082dad7bb4097438c45714bd4fc079a772fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 12 Aug 2019 06:38:18 +0300 Subject: [PATCH 114/273] Type check various base components (#25878) * Type check various component base classes, disabling bunch of checks for now * Type hint fixes * Help mypy out some * Add more type hints --- homeassistant/auth/permissions/__init__.py | 3 ++- homeassistant/components/__init__.py | 3 +++ .../components/automation/__init__.py | 4 +++ homeassistant/components/automation/device.py | 2 ++ homeassistant/components/automation/event.py | 3 +++ .../components/automation/geo_location.py | 3 +++ .../components/automation/homeassistant.py | 3 +++ .../components/automation/litejet.py | 3 +++ homeassistant/components/automation/mqtt.py | 3 +++ .../components/automation/numeric_state.py | 3 +++ homeassistant/components/automation/state.py | 3 +++ homeassistant/components/automation/sun.py | 3 +++ .../components/automation/template.py | 3 +++ homeassistant/components/automation/time.py | 3 +++ .../components/automation/time_pattern.py | 3 +++ .../components/automation/webhook.py | 3 +++ homeassistant/components/automation/zone.py | 3 +++ .../components/binary_sensor/__init__.py | 3 +++ homeassistant/components/calendar/__init__.py | 3 +++ homeassistant/components/camera/__init__.py | 6 ++++- homeassistant/components/camera/prefs.py | 3 +++ homeassistant/components/cover/__init__.py | 3 +++ homeassistant/components/fan/__init__.py | 11 ++++---- homeassistant/components/frontend/__init__.py | 20 +++++++------- homeassistant/components/frontend/storage.py | 3 +++ .../components/geo_location/__init__.py | 3 +++ homeassistant/components/history/__init__.py | 3 +++ homeassistant/components/http/__init__.py | 3 +++ homeassistant/components/http/auth.py | 3 +++ homeassistant/components/http/ban.py | 8 ++++-- homeassistant/components/http/cors.py | 3 +++ .../components/http/data_validator.py | 3 +++ homeassistant/components/http/real_ip.py | 3 +++ homeassistant/components/http/static.py | 6 ++++- homeassistant/components/http/view.py | 8 ++++-- .../components/image_processing/__init__.py | 3 +++ .../components/integration/sensor.py | 3 +++ homeassistant/components/light/__init__.py | 6 ++++- .../components/light/device_automation.py | 3 +++ homeassistant/components/lock/__init__.py | 3 +++ homeassistant/components/mailbox/__init__.py | 3 +++ .../components/media_player/__init__.py | 3 +++ .../media_player/reproduce_state.py | 3 +++ homeassistant/components/mqtt/__init__.py | 8 ++++-- homeassistant/components/notify/__init__.py | 7 ++++- .../components/proximity/__init__.py | 3 +++ homeassistant/components/remote/__init__.py | 3 +++ homeassistant/components/scene/__init__.py | 3 +++ homeassistant/components/sensor/__init__.py | 3 +++ homeassistant/components/switch/__init__.py | 3 +++ homeassistant/components/switch/light.py | 6 ++++- .../components/systemmonitor/sensor.py | 3 +++ homeassistant/components/tts/__init__.py | 9 +++++-- homeassistant/components/vacuum/__init__.py | 3 +++ .../components/water_heater/__init__.py | 3 +++ homeassistant/components/weather/__init__.py | 3 +++ homeassistant/data_entry_flow.py | 8 +++--- homeassistant/scripts/check_config.py | 2 +- mypyrc | 27 +++++++++++++++++++ 59 files changed, 233 insertions(+), 34 deletions(-) diff --git a/homeassistant/auth/permissions/__init__.py b/homeassistant/auth/permissions/__init__.py index 5680b0aecb2..25253a1601c 100644 --- a/homeassistant/auth/permissions/__init__.py +++ b/homeassistant/auth/permissions/__init__.py @@ -7,6 +7,7 @@ from typing import ( # noqa: F401 Dict, List, Mapping, + Optional, Set, Tuple, Union, @@ -31,7 +32,7 @@ _LOGGER = logging.getLogger(__name__) class AbstractPermissions: """Default permissions class.""" - _cached_entity_func = None + _cached_entity_func: Optional[Callable[[str, str], bool]] = None def _entity_func(self) -> Callable[[str, str], bool]: """Return a function that can test entity access.""" diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 8c14276ccc9..7bb572dcf6b 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -11,6 +11,9 @@ import logging from homeassistant.core import split_entity_id + +# mypy: allow-untyped-defs + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 935f6ea0d02..5de9336d1d9 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -30,6 +30,10 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.loader import bind_hass from homeassistant.util.dt import parse_datetime, utcnow + +# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs, no-warn-return-any + DOMAIN = "automation" ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index 2bb70fa1c96..b090484ab67 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -5,6 +5,8 @@ from homeassistant.const import CONF_DOMAIN, CONF_PLATFORM from homeassistant.loader import async_get_integration +# mypy: allow-untyped-defs, no-check-untyped-defs + TRIGGER_SCHEMA = vol.Schema( {vol.Required(CONF_PLATFORM): "device", vol.Required(CONF_DOMAIN): str}, extra=vol.ALLOW_EXTRA, diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index b353eb56196..d372aedd1d7 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -7,6 +7,9 @@ from homeassistant.core import callback from homeassistant.const import CONF_PLATFORM from homeassistant.helpers import config_validation as cv + +# mypy: allow-untyped-defs + CONF_EVENT_TYPE = "event_type" CONF_EVENT_DATA = "event_data" diff --git a/homeassistant/components/automation/geo_location.py b/homeassistant/components/automation/geo_location.py index 7c0994c4b30..3f2aa1c00d7 100644 --- a/homeassistant/components/automation/geo_location.py +++ b/homeassistant/components/automation/geo_location.py @@ -13,6 +13,9 @@ from homeassistant.const import ( from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.config_validation import entity_domain + +# mypy: allow-untyped-defs, no-check-untyped-defs + EVENT_ENTER = "enter" EVENT_LEAVE = "leave" DEFAULT_EVENT = EVENT_ENTER diff --git a/homeassistant/components/automation/homeassistant.py b/homeassistant/components/automation/homeassistant.py index 96931e62766..bd1da7e7e1f 100644 --- a/homeassistant/components/automation/homeassistant.py +++ b/homeassistant/components/automation/homeassistant.py @@ -6,6 +6,9 @@ import voluptuous as vol from homeassistant.core import callback, CoreState from homeassistant.const import CONF_PLATFORM, CONF_EVENT, EVENT_HOMEASSISTANT_STOP + +# mypy: allow-untyped-defs + EVENT_START = "start" EVENT_SHUTDOWN = "shutdown" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py index c642781ca66..7bc4c937765 100644 --- a/homeassistant/components/automation/litejet.py +++ b/homeassistant/components/automation/litejet.py @@ -9,6 +9,9 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util from homeassistant.helpers.event import track_point_in_utc_time + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) CONF_NUMBER = "number" diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index 26c1ea5683d..fd9a778dbfc 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -8,6 +8,9 @@ from homeassistant.components import mqtt from homeassistant.const import CONF_PLATFORM, CONF_PAYLOAD import homeassistant.helpers.config_validation as cv + +# mypy: allow-untyped-defs + CONF_ENCODING = "encoding" CONF_TOPIC = "topic" DEFAULT_ENCODING = "utf-8" diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index f990e599552..b33d724d770 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -16,6 +16,9 @@ from homeassistant.const import ( from homeassistant.helpers.event import async_track_state_change, async_track_same_state from homeassistant.helpers import condition, config_validation as cv, template + +# mypy: allow-untyped-defs, no-check-untyped-defs + TRIGGER_SCHEMA = vol.All( vol.Schema( { diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index ccea3d9ec5a..5fbe97185a7 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -9,6 +9,9 @@ from homeassistant.const import MATCH_ALL, CONF_PLATFORM, CONF_FOR from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.event import async_track_state_change, async_track_same_state + +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) CONF_ENTITY_ID = "entity_id" diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index e4d41830e0f..7cbbe56f326 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -14,6 +14,9 @@ from homeassistant.const import ( from homeassistant.helpers.event import async_track_sunrise, async_track_sunset import homeassistant.helpers.config_validation as cv + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) TRIGGER_SCHEMA = vol.Schema( diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index a48f252312b..c83d660912c 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -10,6 +10,9 @@ from homeassistant.helpers import condition from homeassistant.helpers.event import async_track_same_state, async_track_template from homeassistant.helpers import config_validation as cv, template + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema( diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 958c1f007bc..3942d0efadb 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -8,6 +8,9 @@ from homeassistant.const import CONF_AT, CONF_PLATFORM from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_time_change + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) TRIGGER_SCHEMA = vol.Schema( diff --git a/homeassistant/components/automation/time_pattern.py b/homeassistant/components/automation/time_pattern.py index 15180e07094..f749a308bf7 100644 --- a/homeassistant/components/automation/time_pattern.py +++ b/homeassistant/components/automation/time_pattern.py @@ -8,6 +8,9 @@ from homeassistant.const import CONF_PLATFORM from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_time_change + +# mypy: allow-untyped-defs, no-check-untyped-defs + CONF_HOURS = "hours" CONF_MINUTES = "minutes" CONF_SECONDS = "seconds" diff --git a/homeassistant/components/automation/webhook.py b/homeassistant/components/automation/webhook.py index ceb764cea96..706afbe9042 100644 --- a/homeassistant/components/automation/webhook.py +++ b/homeassistant/components/automation/webhook.py @@ -11,6 +11,9 @@ import homeassistant.helpers.config_validation as cv from . import DOMAIN as AUTOMATION_DOMAIN + +# mypy: allow-untyped-defs + DEPENDENCIES = ("webhook",) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index 1f0f558f0de..35b11006024 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -12,6 +12,9 @@ from homeassistant.const import ( from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers import condition, config_validation as cv, location + +# mypy: allow-untyped-defs, no-check-untyped-defs + EVENT_ENTER = "enter" EVENT_LEAVE = "leave" DEFAULT_EVENT = EVENT_ENTER diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 951f4a423e5..9af6a10c425 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -13,6 +13,9 @@ from homeassistant.helpers.config_validation import ( # noqa PLATFORM_SCHEMA_BASE, ) + +# mypy: allow-untyped-defs, no-check-untyped-defs + DOMAIN = "binary_sensor" SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index e538b6b802a..32817242642 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -17,6 +17,9 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.util import dt + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) DOMAIN = "calendar" diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index efe7a37b310..1b2bfb5fdb1 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -7,6 +7,7 @@ from datetime import timedelta import logging import hashlib from random import SystemRandom +from typing import Deque import attr from aiohttp import web @@ -52,6 +53,9 @@ from homeassistant.setup import async_when_setup from .const import DOMAIN, DATA_CAMERA_PREFS from .prefs import CameraPreferences + +# mypy: allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) SERVICE_ENABLE_MOTION = "enable_motion_detection" @@ -311,7 +315,7 @@ class Camera(Entity): """Initialize a camera.""" self.is_streaming = False self.content_type = DEFAULT_CONTENT_TYPE - self.access_tokens = collections.deque([], 2) + self.access_tokens: Deque[str] = collections.deque([], 2) self.async_update_token() @property diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index 5e22b882d0a..d83e0b55c96 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -1,6 +1,9 @@ """Preference management for camera component.""" from .const import DOMAIN, PREF_PRELOAD_STREAM + +# mypy: allow-untyped-defs, no-check-untyped-defs + STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 _UNDEF = object() diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 696524f5792..d491765bb00 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -32,6 +32,9 @@ from homeassistant.const import ( STATE_CLOSING, ) + +# mypy: allow-untyped-calls, allow-incomplete-defs, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) DOMAIN = "cover" diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 235e8cf5fad..f5edfe5bb59 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import functools as ft import logging +from typing import Optional import voluptuous as vol @@ -74,7 +75,7 @@ FAN_SET_DIRECTION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( @bind_hass -def is_on(hass, entity_id: str = None) -> bool: +def is_on(hass, entity_id: Optional[str] = None) -> bool: """Return if the fans are on based on the statemachine.""" entity_id = entity_id or ENTITY_ID_ALL_FANS state = hass.states.get(entity_id) @@ -149,12 +150,12 @@ class FanEntity(ToggleEntity): return self.hass.async_add_job(self.set_direction, direction) # pylint: disable=arguments-differ - def turn_on(self, speed: str = None, **kwargs) -> None: + def turn_on(self, speed: Optional[str] = None, **kwargs) -> None: """Turn on the fan.""" raise NotImplementedError() # pylint: disable=arguments-differ - def async_turn_on(self, speed: str = None, **kwargs): + def async_turn_on(self, speed: Optional[str] = None, **kwargs): """Turn on the fan. This method must be run in the event loop and returns a coroutine. @@ -180,7 +181,7 @@ class FanEntity(ToggleEntity): return self.speed not in [SPEED_OFF, None] @property - def speed(self) -> str: + def speed(self) -> Optional[str]: """Return the current speed.""" return None @@ -190,7 +191,7 @@ class FanEntity(ToggleEntity): return [] @property - def current_direction(self) -> str: + def current_direction(self) -> Optional[str]: """Return the current direction of the fan.""" return None diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index b769d6b9aea..d8790b746be 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -22,6 +22,8 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage +# mypy: allow-incomplete-defs, allow-untyped-defs, no-check-untyped-defs + # Fix mimetypes for borked Windows machines # https://github.com/home-assistant/home-assistant-polymer/issues/3336 mimetypes.add_type("text/css", ".css") @@ -45,7 +47,14 @@ MANIFEST_JSON = { "description": "Home automation platform that puts local control and privacy first.", "dir": "ltr", "display": "standalone", - "icons": [], + "icons": [ + { + "src": "/static/icons/favicon-{size}x{size}.png".format(size=size), + "sizes": "{size}x{size}".format(size=size), + "type": "image/png", + } + for size in (192, 384, 512, 1024) + ], "lang": "en-US", "name": "Home Assistant", "short_name": "Assistant", @@ -53,15 +62,6 @@ MANIFEST_JSON = { "theme_color": DEFAULT_THEME_COLOR, } -for size in (192, 384, 512, 1024): - MANIFEST_JSON["icons"].append( - { - "src": "/static/icons/favicon-{size}x{size}.png".format(size=size), - "sizes": "{size}x{size}".format(size=size), - "type": "image/png", - } - ) - DATA_PANELS = "frontend_panels" DATA_JS_VERSION = "frontend_js_version" DATA_EXTRA_HTML_URL = "frontend_extra_html_url" diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py index 56f23da5253..75b7b356ef9 100644 --- a/homeassistant/components/frontend/storage.py +++ b/homeassistant/components/frontend/storage.py @@ -4,6 +4,9 @@ import voluptuous as vol from homeassistant.components import websocket_api + +# mypy: allow-untyped-calls, allow-untyped-defs + DATA_STORAGE = "frontend_storage" STORAGE_VERSION_USER_DATA = 1 STORAGE_KEY_USER_DATA = "frontend.user_data_{}" diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index 869f96901c1..3c270f2c521 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -11,6 +11,9 @@ from homeassistant.helpers.config_validation import ( # noqa from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) ATTR_DISTANCE = "distance" diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index d402aceaa40..a2285da4e80 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -21,6 +21,9 @@ from homeassistant.const import ATTR_HIDDEN from homeassistant.components.recorder.util import session_scope, execute import homeassistant.helpers.config_validation as cv + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) DOMAIN = "history" diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 6d31c3fc700..5e474dafa07 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -27,6 +27,9 @@ from .real_ip import setup_real_ip from .static import CACHE_HEADERS, CachingStaticResource from .view import HomeAssistantView # noqa + +# mypy: allow-untyped-defs, no-check-untyped-defs + DOMAIN = "http" CONF_API_PASSWORD = "api_password" diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index c65cb6a2e94..4ff581aef02 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -14,6 +14,9 @@ from homeassistant.util import dt as dt_util from .const import KEY_AUTHENTICATED, KEY_HASS_USER, KEY_REAL_IP + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) DATA_API_PASSWORD = "api_password" diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index db8d2ade959..71e7ff38924 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -3,6 +3,7 @@ from collections import defaultdict from datetime import datetime from ipaddress import ip_address import logging +from typing import List, Optional from aiohttp.web import middleware from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized @@ -16,6 +17,9 @@ from homeassistant.util.yaml import dump from .const import KEY_REAL_IP + +# mypy: allow-incomplete-defs, allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) KEY_BANNED_IPS = "ha_banned_ips" @@ -155,7 +159,7 @@ async def process_success_login(request): class IpBan: """Represents banned IP address.""" - def __init__(self, ip_ban: str, banned_at: datetime = None) -> None: + def __init__(self, ip_ban: str, banned_at: Optional[datetime] = None) -> None: """Initialize IP Ban object.""" self.ip_address = ip_address(ip_ban) self.banned_at = banned_at or datetime.utcnow() @@ -163,7 +167,7 @@ class IpBan: async def async_load_ip_bans_config(hass: HomeAssistant, path: str): """Load list of banned IPs from config file.""" - ip_list = [] + ip_list: List[IpBan] = [] try: list_ = await hass.async_add_executor_job(load_yaml_config_file, path) diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index 19fe88c5cde..39ff45fd4e4 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -5,6 +5,9 @@ from aiohttp.hdrs import ACCEPT, CONTENT_TYPE, ORIGIN, AUTHORIZATION from homeassistant.const import HTTP_HEADER_HA_AUTH, HTTP_HEADER_X_REQUESTED_WITH from homeassistant.core import callback + +# mypy: allow-untyped-defs, no-check-untyped-defs + ALLOWED_CORS_HEADERS = [ ORIGIN, ACCEPT, diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index 8d6ac0b1ceb..634a96aa312 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -4,6 +4,9 @@ import logging import voluptuous as vol + +# mypy: allow-untyped-defs + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/http/real_ip.py b/homeassistant/components/http/real_ip.py index c38e5d0b592..f327c86a4c1 100644 --- a/homeassistant/components/http/real_ip.py +++ b/homeassistant/components/http/real_ip.py @@ -9,6 +9,9 @@ from homeassistant.core import callback from .const import KEY_REAL_IP +# mypy: allow-untyped-defs + + @callback def setup_real_ip(app, use_x_forwarded_for, trusted_proxies): """Create IP Ban middleware for the app.""" diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index f78ce81d884..76844407f7d 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -6,6 +6,9 @@ from aiohttp.web import FileResponse from aiohttp.web_exceptions import HTTPNotFound, HTTPForbidden from aiohttp.web_urldispatcher import StaticResource + +# mypy: allow-untyped-defs + CACHE_TIME = 31 * 86400 # = 1 month CACHE_HEADERS = {hdrs.CACHE_CONTROL: "public, max-age={}".format(CACHE_TIME)} @@ -39,7 +42,8 @@ class CachingStaticResource(StaticResource): if filepath.is_dir(): return await super()._handle(request) if filepath.is_file(): - return FileResponse( + # type ignore: https://github.com/aio-libs/aiohttp/pull/3976 + return FileResponse( # type: ignore filepath, chunk_size=self._chunk_size, headers=CACHE_HEADERS ) raise HTTPNotFound diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 35e74b7c2c0..66864eba55e 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -2,6 +2,7 @@ import asyncio import json import logging +from typing import List, Optional from aiohttp import web from aiohttp.web_exceptions import ( @@ -22,11 +23,14 @@ from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_REAL_IP _LOGGER = logging.getLogger(__name__) +# mypy: allow-untyped-defs, no-check-untyped-defs + + class HomeAssistantView: """Base view for all views.""" - url = None - extra_urls = [] + url: Optional[str] = None + extra_urls: List[str] = [] # Views inheriting from this class can override this requires_auth = True cors_allowed = False diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 1f106e12dcd..b1c167a4175 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -14,6 +14,9 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.util.async_ import run_callback_threadsafe + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) DOMAIN = "image_processing" diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 006e0fe9a41..d24b70c4be0 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -16,6 +16,9 @@ from homeassistant.core import callback from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.restore_state import RestoreEntity + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) ATTR_SOURCE_ID = "source" diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 7ac46d1237d..c70a209a35a 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -4,6 +4,7 @@ import csv from datetime import timedelta import logging import os +from typing import Dict, Optional, Tuple import voluptuous as vol @@ -29,6 +30,9 @@ from homeassistant.helpers import intent from homeassistant.loader import bind_hass import homeassistant.util.color as color_util + +# mypy: allow-untyped-defs, no-check-untyped-defs + DOMAIN = "light" SCAN_INTERVAL = timedelta(seconds=30) @@ -344,7 +348,7 @@ async def async_unload_entry(hass, entry): class Profiles: """Representation of available color profiles.""" - _all = None + _all: Optional[Dict[str, Tuple[float, float, int]]] = None @classmethod async def load_profiles(cls, hass): diff --git a/homeassistant/components/light/device_automation.py b/homeassistant/components/light/device_automation.py index 26ddef16aae..ed75b5f906f 100644 --- a/homeassistant/components/light/device_automation.py +++ b/homeassistant/components/light/device_automation.py @@ -14,6 +14,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_device from . import DOMAIN + +# mypy: allow-untyped-defs, no-check-untyped-defs + CONF_TURN_OFF = "turn_off" CONF_TURN_ON = "turn_on" diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index a2205ccbf46..503bd3a8c78 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -25,6 +25,9 @@ from homeassistant.const import ( ) from homeassistant.components import group + +# mypy: allow-untyped-defs, no-check-untyped-defs + ATTR_CHANGED_BY = "changed_by" DOMAIN = "lock" diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 08853a4edce..1252036e1b2 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -16,6 +16,9 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.setup import async_prepare_setup_platform + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) DOMAIN = "mailbox" diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 8334577ac30..791dacb7024 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -97,6 +97,9 @@ from .const import ( SUPPORT_VOLUME_STEP, ) + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) _RND = SystemRandom() diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index a926274e641..4eba4657d95 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -36,6 +36,9 @@ from .const import ( ) +# mypy: allow-incomplete-defs, allow-untyped-defs + + async def _async_reproduce_states( hass: HomeAssistantType, state: State, context: Optional[Context] = None ) -> None: diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index af2fbac9639..75552d1d14b 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -416,7 +416,7 @@ async def async_subscribe( topic: str, msg_callback: MessageCallbackType, qos: int = DEFAULT_QOS, - encoding: str = "utf-8", + encoding: Optional[str] = "utf-8", ): """Subscribe to an MQTT topic. @@ -829,7 +829,11 @@ class MQTT: return self.hass.async_add_job(stop) async def async_subscribe( - self, topic: str, msg_callback: MessageCallbackType, qos: int, encoding: str + self, + topic: str, + msg_callback: MessageCallbackType, + qos: int, + encoding: Optional[str] = None, ) -> Callable[[], None]: """Set up a subscription to a topic with the provided qos. diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 8483b703a70..773c08808c3 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -2,6 +2,7 @@ import asyncio import logging from functools import partial +from typing import Optional import voluptuous as vol @@ -10,8 +11,12 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME, CONF_PLATFORM from homeassistant.helpers import config_per_platform, discovery +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) # Platform specific data @@ -164,7 +169,7 @@ async def async_setup(hass, config): class BaseNotificationService: """An abstract class for notification services.""" - hass = None + hass: Optional[HomeAssistantType] = None def send_message(self, message, **kwargs): """Send a message. diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index d563314225a..e9b85f79084 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -10,6 +10,9 @@ from homeassistant.helpers.event import track_state_change from homeassistant.util.distance import convert from homeassistant.util.location import distance + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) ATTR_DIR_OF_TRAVEL = "dir_of_travel" diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index e495874f339..450b1c123c3 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -22,6 +22,9 @@ from homeassistant.helpers.config_validation import ( # noqa PLATFORM_SCHEMA_BASE, ) + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) ATTR_ACTIVITY = "activity" diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 5ddb1116d8f..1f71a24c304 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -12,6 +12,9 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.state import HASS_DOMAIN + +# mypy: allow-untyped-defs, no-check-untyped-defs + DOMAIN = "scene" STATE = "scening" STATES = "states" diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index cc460cdd4a9..9ca11b5266a 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -21,6 +21,9 @@ from homeassistant.helpers.config_validation import ( # noqa ) from homeassistant.helpers.entity_component import EntityComponent + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) DOMAIN = "sensor" diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index a52380882db..348c2a8616b 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -20,6 +20,9 @@ from homeassistant.const import ( ) from homeassistant.components import group + +# mypy: allow-untyped-defs, no-check-untyped-defs + DOMAIN = "switch" SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 38c66854a0b..0b1094c0dd9 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -1,5 +1,6 @@ """Light support for switch entities.""" import logging +from typing import cast import voluptuous as vol @@ -18,6 +19,9 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.components.light import PLATFORM_SCHEMA, Light + +# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Light Switch" @@ -35,7 +39,7 @@ async def async_setup_platform( ) -> None: """Initialize Light Switch platform.""" async_add_entities( - [LightSwitch(config.get(CONF_NAME), config[CONF_ENTITY_ID])], True + [LightSwitch(cast(str, config.get(CONF_NAME)), config[CONF_ENTITY_ID])], True ) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 207440a6646..446a36ec350 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -12,6 +12,9 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) CONF_ARG = "arg" diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 862c5e9cf9a..77d24fd7aab 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -8,6 +8,7 @@ import logging import mimetypes import os import re +from typing import Optional from aiohttp import web import voluptuous as vol @@ -25,8 +26,12 @@ from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_prepare_setup_platform + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) ATTR_CACHE = "cache" @@ -461,8 +466,8 @@ class SpeechManager: class Provider: """Represent a single TTS provider.""" - hass = None - name = None + hass: Optional[HomeAssistantType] = None + name: Optional[str] = None @property def default_language(self): diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index ea2f29ed225..598c068aad5 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -27,6 +27,9 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import ToggleEntity, Entity from homeassistant.helpers.icon import icon_for_battery_level + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) DOMAIN = "vacuum" diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index d4402eec3b5..c41381fe5fa 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -27,6 +27,9 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) + +# mypy: allow-untyped-defs, no-check-untyped-defs + DEFAULT_MIN_TEMP = 110 DEFAULT_MAX_TEMP = 140 diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 0bd22304119..8f276279ee5 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -11,6 +11,9 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.temperature import display_temp as show_temp + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) ATTR_CONDITION_CLASS = "condition_class" diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 789093cc49c..7ce27d404cf 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -175,11 +175,11 @@ class FlowHandler: """Handle the configuration flow of a component.""" # Set by flow manager - flow_id = None - hass = None + flow_id: Optional[str] = None + hass: Optional[HomeAssistant] = None handler = None - cur_step = None - context = None # type: Optional[Dict] + cur_step: Optional[Dict[str, str]] = None + context: Optional[Dict] = None # Set by _async_create_flow callback init_step = "init" diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index e8493822771..28734b30fcc 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -28,7 +28,7 @@ MOCKS = { } # type: Dict[str, Tuple[str, Callable]] SILENCE = ("homeassistant.scripts.check_config.yaml_loader.clear_secret_cache",) -PATCHES = {} +PATCHES: Dict[str, Any] = {} C_HEAD = "bold" ERROR_STR = "General Errors" diff --git a/mypyrc b/mypyrc index ece4678abe6..f3866f40e57 100644 --- a/mypyrc +++ b/mypyrc @@ -1,5 +1,32 @@ homeassistant/*.py homeassistant/auth/ +homeassistant/components/*.py +homeassistant/components/automation/ +homeassistant/components/binary_sensor/ +homeassistant/components/calendar/ +homeassistant/components/camera/ +homeassistant/components/cover/ +homeassistant/components/frontend/ +homeassistant/components/geo_location/ +homeassistant/components/history/ +homeassistant/components/http/ +homeassistant/components/image_processing/ +homeassistant/components/integration/ +homeassistant/components/light/ +homeassistant/components/lock/ +homeassistant/components/mailbox/ +homeassistant/components/media_player/ +homeassistant/components/notify/ +homeassistant/components/proximity/ +homeassistant/components/remote/ +homeassistant/components/scene/ +homeassistant/components/sensor/ +homeassistant/components/switch/ +homeassistant/components/systemmonitor/ +homeassistant/components/tts/ +homeassistant/components/vacuum/ +homeassistant/components/water_heater/ +homeassistant/components/weather/ homeassistant/helpers/ homeassistant/scripts/ homeassistant/util/ From ec2ce312046a14a8877c524583a756efb3327d9c Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 12 Aug 2019 05:40:18 +0200 Subject: [PATCH 115/273] Fix script/translations_develop launch + clean some unused error i18n (#25459) * Fix : script/translations_develop launch + clean some unused error i18n * Respecting strings.json order property title step error abort * Review from @fredrike : remove Telldus Live all_configured --- .../arcam_fmj/.translations/ca.json | 5 --- .../arcam_fmj/.translations/ko.json | 5 --- .../arcam_fmj/.translations/nl.json | 17 -------- .../arcam_fmj/.translations/pl.json | 23 ----------- .../arcam_fmj/.translations/zh-Hant.json | 5 --- .../components/arcam_fmj/strings.json | 5 +-- homeassistant/components/heos/strings.json | 33 ++++++++-------- .../tellduslive/.translations/bg.json | 1 - .../tellduslive/.translations/ca.json | 1 - .../tellduslive/.translations/da.json | 1 - .../tellduslive/.translations/de.json | 1 - .../tellduslive/.translations/en.json | 1 - .../tellduslive/.translations/es-419.json | 1 - .../tellduslive/.translations/es.json | 1 - .../tellduslive/.translations/fr.json | 1 - .../tellduslive/.translations/hu.json | 1 - .../tellduslive/.translations/it.json | 1 - .../tellduslive/.translations/ko.json | 1 - .../tellduslive/.translations/lb.json | 1 - .../tellduslive/.translations/nl.json | 1 - .../tellduslive/.translations/no.json | 1 - .../tellduslive/.translations/pl.json | 1 - .../tellduslive/.translations/pt-BR.json | 1 - .../tellduslive/.translations/pt.json | 1 - .../tellduslive/.translations/ru.json | 1 - .../tellduslive/.translations/sl.json | 1 - .../tellduslive/.translations/sv.json | 1 - .../tellduslive/.translations/zh-Hans.json | 1 - .../tellduslive/.translations/zh-Hant.json | 1 - .../twentemilieu/.translations/en.json | 30 +++++++------- .../components/twentemilieu/strings.json | 2 +- .../components/upnp/.translations/da.json | 4 -- .../components/upnp/.translations/de.json | 4 -- .../components/upnp/.translations/es.json | 4 -- .../components/upnp/.translations/hu.json | 4 -- .../components/upnp/.translations/lb.json | 4 -- .../components/upnp/.translations/nl.json | 4 -- .../components/upnp/.translations/nn.json | 4 -- .../components/upnp/.translations/no.json | 8 ---- .../components/upnp/.translations/pl.json | 6 --- .../components/upnp/.translations/pt.json | 4 -- .../components/upnp/.translations/ro.json | 5 --- .../components/upnp/.translations/sl.json | 6 --- .../components/upnp/.translations/sv.json | 4 -- homeassistant/components/upnp/strings.json | 39 +++++++++++++------ .../components/vesync/.translations/en.json | 24 ++++++------ homeassistant/components/vesync/strings.json | 32 +++++++-------- homeassistant/components/zha/strings.json | 33 ++++++++-------- 48 files changed, 105 insertions(+), 231 deletions(-) delete mode 100644 homeassistant/components/arcam_fmj/.translations/ca.json delete mode 100644 homeassistant/components/arcam_fmj/.translations/ko.json delete mode 100644 homeassistant/components/arcam_fmj/.translations/nl.json delete mode 100644 homeassistant/components/arcam_fmj/.translations/pl.json delete mode 100644 homeassistant/components/arcam_fmj/.translations/zh-Hant.json diff --git a/homeassistant/components/arcam_fmj/.translations/ca.json b/homeassistant/components/arcam_fmj/.translations/ca.json deleted file mode 100644 index b0ad4660d0f..00000000000 --- a/homeassistant/components/arcam_fmj/.translations/ca.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "config": { - "title": "Arcam FMJ" - } -} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/ko.json b/homeassistant/components/arcam_fmj/.translations/ko.json deleted file mode 100644 index b0ad4660d0f..00000000000 --- a/homeassistant/components/arcam_fmj/.translations/ko.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "config": { - "title": "Arcam FMJ" - } -} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/nl.json b/homeassistant/components/arcam_fmj/.translations/nl.json deleted file mode 100644 index 7197976d212..00000000000 --- a/homeassistant/components/arcam_fmj/.translations/nl.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "one": "Een", - "other": "Ander" - }, - "error": { - "one": "Een", - "other": "Ander" - }, - "step": { - "one": "Een", - "other": "Ander" - }, - "title": "Arcam FMJ" - } -} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/pl.json b/homeassistant/components/arcam_fmj/.translations/pl.json deleted file mode 100644 index 5521c18c079..00000000000 --- a/homeassistant/components/arcam_fmj/.translations/pl.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "few": "kilka", - "many": "wiele", - "one": "jeden", - "other": "inne" - }, - "error": { - "few": "kilka", - "many": "wiele", - "one": "jeden", - "other": "inne" - }, - "step": { - "few": "kilka", - "many": "wiele", - "one": "jeden", - "other": "inne" - }, - "title": "Arcam FMJ" - } -} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/zh-Hant.json b/homeassistant/components/arcam_fmj/.translations/zh-Hant.json deleted file mode 100644 index b0ad4660d0f..00000000000 --- a/homeassistant/components/arcam_fmj/.translations/zh-Hant.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "config": { - "title": "Arcam FMJ" - } -} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/strings.json b/homeassistant/components/arcam_fmj/strings.json index 5844c277364..b0006dbb5ae 100644 --- a/homeassistant/components/arcam_fmj/strings.json +++ b/homeassistant/components/arcam_fmj/strings.json @@ -1,8 +1,5 @@ { "config": { - "title": "Arcam FMJ", - "step": {}, - "error": {}, - "abort": {} + "title": "Arcam FMJ" } } diff --git a/homeassistant/components/heos/strings.json b/homeassistant/components/heos/strings.json index b210e0ba87f..9a00ac6a4bd 100644 --- a/homeassistant/components/heos/strings.json +++ b/homeassistant/components/heos/strings.json @@ -1,20 +1,21 @@ { - "config": { - "title": "HEOS", - "step": { - "user": { - "title": "Connect to Heos", - "description": "Please enter the host name or IP address of a Heos device (preferably one connected via wire to the network).", - "data": { - "host": "Host" + "config": { + "title": "HEOS", + "step": { + "user": { + "title": "Connect to Heos", + "description": "Please enter the host name or IP address of a Heos device (preferably one connected via wire to the network).", + "data": { + "access_token": "Host", + "host": "Host" + } + } + }, + "error": { + "connection_failure": "Unable to connect to the specified host." + }, + "abort": { + "already_setup": "You can only configure a single Heos connection as it will support all devices on the network." } - } - }, - "error": { - "connection_failure": "Unable to connect to the specified host." - }, - "abort": { - "already_setup": "You can only configure a single Heos connection as it will support all devices on the network." } - } } \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/bg.json b/homeassistant/components/tellduslive/.translations/bg.json index 3e493a3973a..46ae4eba463 100644 --- a/homeassistant/components/tellduslive/.translations/bg.json +++ b/homeassistant/components/tellduslive/.translations/bg.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "already_setup": "TelldusLive \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f.", "authorize_url_timeout": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0432 \u0441\u0440\u043e\u043a.", diff --git a/homeassistant/components/tellduslive/.translations/ca.json b/homeassistant/components/tellduslive/.translations/ca.json index 437b9b460d2..fafa8798401 100644 --- a/homeassistant/components/tellduslive/.translations/ca.json +++ b/homeassistant/components/tellduslive/.translations/ca.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive ja est\u00e0 configurat", "already_setup": "TelldusLive ja est\u00e0 configurat", "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", "authorize_url_timeout": "S'ha acabat el temps d'espera mentre \u00e9s generava l'URL d'autoritzaci\u00f3.", diff --git a/homeassistant/components/tellduslive/.translations/da.json b/homeassistant/components/tellduslive/.translations/da.json index 717e3ec5ac9..895570c3698 100644 --- a/homeassistant/components/tellduslive/.translations/da.json +++ b/homeassistant/components/tellduslive/.translations/da.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive er allerede konfigureret", "already_setup": "TelldusLive er allerede konfigureret", "authorize_url_fail": "Ukendt fejl ved generering af en autoriseret url.", "authorize_url_timeout": "Timeout ved generering af autoriseret url.", diff --git a/homeassistant/components/tellduslive/.translations/de.json b/homeassistant/components/tellduslive/.translations/de.json index 6c094ed6a8c..18c3e88666e 100644 --- a/homeassistant/components/tellduslive/.translations/de.json +++ b/homeassistant/components/tellduslive/.translations/de.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive ist bereits konfiguriert", "already_setup": "TelldusLive ist bereits konfiguriert", "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", diff --git a/homeassistant/components/tellduslive/.translations/en.json b/homeassistant/components/tellduslive/.translations/en.json index c2b00561858..4ed9ef597f4 100644 --- a/homeassistant/components/tellduslive/.translations/en.json +++ b/homeassistant/components/tellduslive/.translations/en.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive is already configured", "already_setup": "TelldusLive is already configured", "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize url.", diff --git a/homeassistant/components/tellduslive/.translations/es-419.json b/homeassistant/components/tellduslive/.translations/es-419.json index bf74d104835..503530e728a 100644 --- a/homeassistant/components/tellduslive/.translations/es-419.json +++ b/homeassistant/components/tellduslive/.translations/es-419.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive ya est\u00e1 configurado", "already_setup": "TelldusLive ya est\u00e1 configurado", "unknown": "Se produjo un error desconocido" }, diff --git a/homeassistant/components/tellduslive/.translations/es.json b/homeassistant/components/tellduslive/.translations/es.json index 6b3cea7f484..b0313a1eee3 100644 --- a/homeassistant/components/tellduslive/.translations/es.json +++ b/homeassistant/components/tellduslive/.translations/es.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive ya est\u00e1 configurado", "already_setup": "TelldusLive ya est\u00e1 configurado", "authorize_url_fail": "Error desconocido generando la url de autorizaci\u00f3n", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n", diff --git a/homeassistant/components/tellduslive/.translations/fr.json b/homeassistant/components/tellduslive/.translations/fr.json index a7ddd4c6fa6..70f4ef6f1d9 100644 --- a/homeassistant/components/tellduslive/.translations/fr.json +++ b/homeassistant/components/tellduslive/.translations/fr.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive est d\u00e9j\u00e0 configur\u00e9", "already_setup": "TelldusLive est d\u00e9j\u00e0 configur\u00e9", "authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.", "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", diff --git a/homeassistant/components/tellduslive/.translations/hu.json b/homeassistant/components/tellduslive/.translations/hu.json index cd219be04e1..8519f02ef5b 100644 --- a/homeassistant/components/tellduslive/.translations/hu.json +++ b/homeassistant/components/tellduslive/.translations/hu.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "A TelldusLive-ot m\u00e1r be\u00e1ll\u00edtottuk.", "already_setup": "A TelldusLive m\u00e1r be van \u00e1ll\u00edtva", "authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", diff --git a/homeassistant/components/tellduslive/.translations/it.json b/homeassistant/components/tellduslive/.translations/it.json index 90f13184a67..3baa307de51 100644 --- a/homeassistant/components/tellduslive/.translations/it.json +++ b/homeassistant/components/tellduslive/.translations/it.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive \u00e8 gi\u00e0 configurato", "already_setup": "TelldusLive \u00e8 gi\u00e0 configurato", "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", "authorize_url_timeout": "Tempo scaduto nel generare l'url di autorizzazione", diff --git a/homeassistant/components/tellduslive/.translations/ko.json b/homeassistant/components/tellduslive/.translations/ko.json index 6b04e867861..10e289f2520 100644 --- a/homeassistant/components/tellduslive/.translations/ko.json +++ b/homeassistant/components/tellduslive/.translations/ko.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "already_setup": "TelldusLive \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", diff --git a/homeassistant/components/tellduslive/.translations/lb.json b/homeassistant/components/tellduslive/.translations/lb.json index 4584635066c..a01436f9ba8 100644 --- a/homeassistant/components/tellduslive/.translations/lb.json +++ b/homeassistant/components/tellduslive/.translations/lb.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive ass scho konfigur\u00e9iert", "already_setup": "TelldusLive ass scho konfigur\u00e9iert", "authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", diff --git a/homeassistant/components/tellduslive/.translations/nl.json b/homeassistant/components/tellduslive/.translations/nl.json index a1029d991fe..fac9475f6f3 100644 --- a/homeassistant/components/tellduslive/.translations/nl.json +++ b/homeassistant/components/tellduslive/.translations/nl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive is al geconfigureerd", "already_setup": "TelldusLive is al geconfigureerd", "authorize_url_fail": "Onbekende fout bij het genereren van een autorisatie url.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", diff --git a/homeassistant/components/tellduslive/.translations/no.json b/homeassistant/components/tellduslive/.translations/no.json index 2c6439b364f..d311b3b0d38 100644 --- a/homeassistant/components/tellduslive/.translations/no.json +++ b/homeassistant/components/tellduslive/.translations/no.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive er allerede konfigurert", "already_setup": "TelldusLive er allerede konfigurert", "authorize_url_fail": "Ukjent feil ved generering av autoriseringsadresse.", "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", diff --git a/homeassistant/components/tellduslive/.translations/pl.json b/homeassistant/components/tellduslive/.translations/pl.json index 9d791e0e786..06391b24b99 100644 --- a/homeassistant/components/tellduslive/.translations/pl.json +++ b/homeassistant/components/tellduslive/.translations/pl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive jest ju\u017c skonfigurowany", "already_setup": "TelldusLive jest ju\u017c skonfigurowany", "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", diff --git a/homeassistant/components/tellduslive/.translations/pt-BR.json b/homeassistant/components/tellduslive/.translations/pt-BR.json index 4a3983facf6..2183b9068b8 100644 --- a/homeassistant/components/tellduslive/.translations/pt-BR.json +++ b/homeassistant/components/tellduslive/.translations/pt-BR.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive j\u00e1 est\u00e1 configurado", "already_setup": "TelldusLive j\u00e1 est\u00e1 configurado", "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Tempo limite de gera\u00e7\u00e3o de url de autoriza\u00e7\u00e3o.", diff --git a/homeassistant/components/tellduslive/.translations/pt.json b/homeassistant/components/tellduslive/.translations/pt.json index a13f71f7505..9f8134ed07d 100644 --- a/homeassistant/components/tellduslive/.translations/pt.json +++ b/homeassistant/components/tellduslive/.translations/pt.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive j\u00e1 est\u00e1 configurado", "already_setup": "TelldusLive j\u00e1 est\u00e1 configurado", "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o.", diff --git a/homeassistant/components/tellduslive/.translations/ru.json b/homeassistant/components/tellduslive/.translations/ru.json index 3b34e048b11..afaaf4edbf5 100644 --- a/homeassistant/components/tellduslive/.translations/ru.json +++ b/homeassistant/components/tellduslive/.translations/ru.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", diff --git a/homeassistant/components/tellduslive/.translations/sl.json b/homeassistant/components/tellduslive/.translations/sl.json index 16e6ddcb5f4..7e0a8d28b9a 100644 --- a/homeassistant/components/tellduslive/.translations/sl.json +++ b/homeassistant/components/tellduslive/.translations/sl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive je \u017ee konfiguriran", "already_setup": "TelldusLive je \u017ee konfiguriran", "authorize_url_fail": "Neznana napaka pri generiranju potrditvenega URL-ja.", "authorize_url_timeout": "\u010casovna omejitev za generiranje URL-ja je potekla.", diff --git a/homeassistant/components/tellduslive/.translations/sv.json b/homeassistant/components/tellduslive/.translations/sv.json index 5636e137948..809132e01b0 100644 --- a/homeassistant/components/tellduslive/.translations/sv.json +++ b/homeassistant/components/tellduslive/.translations/sv.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "Telldus Live! \u00e4r redan konfigurerad", "already_setup": "Telldus Live! \u00e4r redan konfigurerad", "authorize_url_fail": "Ok\u00e4nt fel n\u00e4r genererar en url f\u00f6r att auktorisera.", "authorize_url_timeout": "Timeout n\u00e4r genererar auktorisera url.", diff --git a/homeassistant/components/tellduslive/.translations/zh-Hans.json b/homeassistant/components/tellduslive/.translations/zh-Hans.json index e447fcf92ef..657722f7f13 100644 --- a/homeassistant/components/tellduslive/.translations/zh-Hans.json +++ b/homeassistant/components/tellduslive/.translations/zh-Hans.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "Tellduslive \u5df2\u914d\u7f6e\u5b8c\u6210", "already_setup": "TelldusLive \u5df2\u914d\u7f6e\u5b8c\u6210", "authorize_url_fail": "\u751f\u6210\u6388\u6743\u7f51\u5740\u65f6\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002", "authorize_url_timeout": "\u751f\u6210\u6388\u6743\u7f51\u5740\u8d85\u65f6\u3002", diff --git a/homeassistant/components/tellduslive/.translations/zh-Hant.json b/homeassistant/components/tellduslive/.translations/zh-Hant.json index c95e96b21c9..23093d247ee 100644 --- a/homeassistant/components/tellduslive/.translations/zh-Hant.json +++ b/homeassistant/components/tellduslive/.translations/zh-Hant.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_setup": "TelldusLive \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", diff --git a/homeassistant/components/twentemilieu/.translations/en.json b/homeassistant/components/twentemilieu/.translations/en.json index deabeeeb835..ce969a4f464 100644 --- a/homeassistant/components/twentemilieu/.translations/en.json +++ b/homeassistant/components/twentemilieu/.translations/en.json @@ -1,23 +1,23 @@ { "config": { - "title": "Twente Milieu", - "step": { - "user": { - "title": "Twente Milieu", - "description": "Set up Twente Milieu providing waste collection information on your address.", - "data": { - "post_code": "Postal code", - "house_number": "House number", - "house_letter": "House letter/additional" - } - } + "abort": { + "address_exists": "Address already set up." }, "error": { "connection_error": "Failed to connect.", "invalid_address": "Address not found in Twente Milieu service area." }, - "abort": { - "address_already_set_up": "Address already set up." - } + "step": { + "user": { + "data": { + "house_letter": "House letter/additional", + "house_number": "House number", + "post_code": "Postal code" + }, + "description": "Set up Twente Milieu providing waste collection information on your address.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" } -} +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/strings.json b/homeassistant/components/twentemilieu/strings.json index b94b7146d2c..811ecdbfa6e 100644 --- a/homeassistant/components/twentemilieu/strings.json +++ b/homeassistant/components/twentemilieu/strings.json @@ -20,4 +20,4 @@ "address_exists": "Address already set up." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/da.json b/homeassistant/components/upnp/.translations/da.json index 1d0097c2f1f..778834d24eb 100644 --- a/homeassistant/components/upnp/.translations/da.json +++ b/homeassistant/components/upnp/.translations/da.json @@ -8,10 +8,6 @@ "no_sensors_or_port_mapping": "Aktiv\u00e9r enten sensorer eller porttilknytning", "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af UPnP/IGD." }, - "error": { - "one": "En", - "other": "Anden" - }, "step": { "confirm": { "description": "Er du sikker p\u00e5 at du vil konfigurere UPnP/IGD?", diff --git a/homeassistant/components/upnp/.translations/de.json b/homeassistant/components/upnp/.translations/de.json index 907bfffbeea..51faf56367d 100644 --- a/homeassistant/components/upnp/.translations/de.json +++ b/homeassistant/components/upnp/.translations/de.json @@ -8,10 +8,6 @@ "no_sensors_or_port_mapping": "Aktiviere mindestens Sensoren oder Port-Mapping", "single_instance_allowed": "Es ist nur eine einzige Konfiguration von UPnP/IGD erforderlich." }, - "error": { - "one": "Ein", - "other": "andere" - }, "step": { "confirm": { "description": "M\u00f6chten Sie UPnP/IGD einrichten?", diff --git a/homeassistant/components/upnp/.translations/es.json b/homeassistant/components/upnp/.translations/es.json index fa299cc379f..99b1065fb89 100644 --- a/homeassistant/components/upnp/.translations/es.json +++ b/homeassistant/components/upnp/.translations/es.json @@ -8,10 +8,6 @@ "no_sensors_or_port_mapping": "Habilitar al menos sensores o mapeo de puertos", "single_instance_allowed": "S\u00f3lo se necesita una configuraci\u00f3n de UPnP/IGD." }, - "error": { - "one": "UNO", - "other": "OTRO" - }, "step": { "confirm": { "description": "\u00bfDesea configurar UPnP/IGD?", diff --git a/homeassistant/components/upnp/.translations/hu.json b/homeassistant/components/upnp/.translations/hu.json index 29dab5e09da..b4785f2f625 100644 --- a/homeassistant/components/upnp/.translations/hu.json +++ b/homeassistant/components/upnp/.translations/hu.json @@ -7,10 +7,6 @@ "no_devices_found": "Nincsenek UPnPIGD eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton.", "single_instance_allowed": "Csak egy UPnP / IGD konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges." }, - "error": { - "one": "hiba", - "other": "" - }, "step": { "confirm": { "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a UPnP/IGD-t?", diff --git a/homeassistant/components/upnp/.translations/lb.json b/homeassistant/components/upnp/.translations/lb.json index 029e1e87cf1..55d41e588f6 100644 --- a/homeassistant/components/upnp/.translations/lb.json +++ b/homeassistant/components/upnp/.translations/lb.json @@ -8,10 +8,6 @@ "no_sensors_or_port_mapping": "Aktiv\u00e9ier op mannst Sensoren oder Port Mapping", "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun UPnP/IGD ass n\u00e9ideg." }, - "error": { - "one": "Een", - "other": "Aaner" - }, "step": { "confirm": { "description": "Soll UPnP/IGD konfigur\u00e9iert ginn?", diff --git a/homeassistant/components/upnp/.translations/nl.json b/homeassistant/components/upnp/.translations/nl.json index a94471bb610..5d426f2edaf 100644 --- a/homeassistant/components/upnp/.translations/nl.json +++ b/homeassistant/components/upnp/.translations/nl.json @@ -8,10 +8,6 @@ "no_sensors_or_port_mapping": "Schakel ten minste sensoren of poorttoewijzing in", "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van UPnP/IGD nodig." }, - "error": { - "one": "Een", - "other": "Ander" - }, "step": { "confirm": { "description": "Wilt u UPnP/IGD instellen?", diff --git a/homeassistant/components/upnp/.translations/nn.json b/homeassistant/components/upnp/.translations/nn.json index 286efcf0353..63339244116 100644 --- a/homeassistant/components/upnp/.translations/nn.json +++ b/homeassistant/components/upnp/.translations/nn.json @@ -3,10 +3,6 @@ "abort": { "no_sensors_or_port_mapping": "I det minste, aktiver sensor eller portkartlegging" }, - "error": { - "one": "Ein", - "other": "Andre" - }, "step": { "init": { "title": "UPnP / IGD" diff --git a/homeassistant/components/upnp/.translations/no.json b/homeassistant/components/upnp/.translations/no.json index 813509121e3..b3b6ef17366 100644 --- a/homeassistant/components/upnp/.translations/no.json +++ b/homeassistant/components/upnp/.translations/no.json @@ -8,14 +8,6 @@ "no_sensors_or_port_mapping": "Aktiver minst sensorer eller port mapping", "single_instance_allowed": "Bare en enkelt konfigurasjon av UPnP / IGD er n\u00f8dvendig." }, - "error": { - "few": "f\u00e5", - "many": "mange", - "one": "en", - "other": "andre", - "two": "to", - "zero": "ingen" - }, "step": { "confirm": { "description": "\u00d8nsker du \u00e5 konfigurere UPnP / IGD?", diff --git a/homeassistant/components/upnp/.translations/pl.json b/homeassistant/components/upnp/.translations/pl.json index d7ede44d22d..94823bd90d3 100644 --- a/homeassistant/components/upnp/.translations/pl.json +++ b/homeassistant/components/upnp/.translations/pl.json @@ -8,12 +8,6 @@ "no_sensors_or_port_mapping": "W\u0142\u0105cz przynajmniej sensory lub mapowanie port\u00f3w", "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja UPnP/IGD." }, - "error": { - "few": "kilka", - "many": "wiele", - "one": "jeden", - "other": "inne" - }, "step": { "confirm": { "description": "Czy chcesz skonfigurowa\u0107 UPnP/IGD?", diff --git a/homeassistant/components/upnp/.translations/pt.json b/homeassistant/components/upnp/.translations/pt.json index d559a05ff23..698a0d98f3d 100644 --- a/homeassistant/components/upnp/.translations/pt.json +++ b/homeassistant/components/upnp/.translations/pt.json @@ -8,10 +8,6 @@ "no_sensors_or_port_mapping": "Ative pelo menos os sensores ou o mapeamento de porta", "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do UPnP/IGD \u00e9 necess\u00e1ria." }, - "error": { - "one": "um", - "other": "v\u00e1rios" - }, "step": { "confirm": { "description": "Deseja configurar o UPnP / IGD?", diff --git a/homeassistant/components/upnp/.translations/ro.json b/homeassistant/components/upnp/.translations/ro.json index bb584da05dc..1a328adb9ef 100644 --- a/homeassistant/components/upnp/.translations/ro.json +++ b/homeassistant/components/upnp/.translations/ro.json @@ -4,11 +4,6 @@ "already_configured": "UPnP/IGD este deja configurat", "no_devices_discovered": "Nu au fost descoperite UPnP/IGD-uri" }, - "error": { - "few": "", - "one": "Unul", - "other": "" - }, "step": { "init": { "title": "UPnP/IGD" diff --git a/homeassistant/components/upnp/.translations/sl.json b/homeassistant/components/upnp/.translations/sl.json index 4c019d8f207..b31a7e792ea 100644 --- a/homeassistant/components/upnp/.translations/sl.json +++ b/homeassistant/components/upnp/.translations/sl.json @@ -8,12 +8,6 @@ "no_sensors_or_port_mapping": "Omogo\u010dite vsaj senzorje ali preslikavo vrat (port mapping)", "single_instance_allowed": "Potrebna je samo ena konfiguracija UPnp/IGD." }, - "error": { - "few": "nekaj", - "one": "ena", - "other": "ve\u010d", - "two": "dve" - }, "step": { "confirm": { "description": "Ali \u017eelite nastaviti UPnp/IGD?", diff --git a/homeassistant/components/upnp/.translations/sv.json b/homeassistant/components/upnp/.translations/sv.json index e3864aee4da..c2447be1216 100644 --- a/homeassistant/components/upnp/.translations/sv.json +++ b/homeassistant/components/upnp/.translations/sv.json @@ -8,10 +8,6 @@ "no_sensors_or_port_mapping": "Aktivera minst sensorer eller portmappning", "single_instance_allowed": "Endast en enda konfiguration av UPnP/IGD \u00e4r n\u00f6dv\u00e4ndig." }, - "error": { - "one": "En", - "other": "Andra" - }, "step": { "confirm": { "description": "Vill du konfigurera UPnP/IGD?", diff --git a/homeassistant/components/upnp/strings.json b/homeassistant/components/upnp/strings.json index f4de9ad4c0d..6c5b2fb2bb2 100644 --- a/homeassistant/components/upnp/strings.json +++ b/homeassistant/components/upnp/strings.json @@ -1,15 +1,30 @@ { - "config": { - "title": "UPnP/IGD", - "step": { - "confirm": { + "config": { "title": "UPnP/IGD", - "description": "Do you want to set up UPnP/IGD?" - } - }, - "abort": { - "single_instance_allowed": "Only a single configuration of UPnP/IGD is necessary.", - "no_devices_found": "No UPnP/IGD devices found on the network." + "step": { + "init": { + "title": "UPnP/IGD" + }, + "confirm": { + "title": "UPnP/IGD", + "description": "Do you want to set up UPnP/IGD?" + }, + "user": { + "title": "Configuration options for the UPnP/IGD", + "data": { + "enable_port_mapping": "Enable port mapping for Home Assistant", + "enable_sensors": "Add traffic sensors", + "igd": "UPnP/IGD" + } + } + }, + "abort": { + "already_configured": "UPnP/IGD is already configured", + "incomplete_device": "Ignoring incomplete UPnP device", + "no_devices_discovered": "No UPnP/IGDs discovered", + "no_devices_found": "No UPnP/IGD devices found on the network.", + "no_sensors_or_port_mapping": "Enable at least sensors or port mapping", + "single_instance_allowed": "Only a single configuration of UPnP/IGD is necessary." + } } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/en.json b/homeassistant/components/vesync/.translations/en.json index ec0bc728cdb..cd8b3e59cbf 100644 --- a/homeassistant/components/vesync/.translations/en.json +++ b/homeassistant/components/vesync/.translations/en.json @@ -1,20 +1,20 @@ { "config": { - "title": "VeSync", - "step": { - "user": { - "title": "Enter Username and Password", - "data": { - "username": "Email Address", - "password": "Password" - } - } + "abort": { + "already_setup": "Only one Vesync instance is allowed" }, "error": { "invalid_login": "Invalid username or password" }, - "abort": { - "already_setup": "Only one Vesync instance is allow" - } + "step": { + "user": { + "data": { + "password": "Password", + "username": "Email Address" + }, + "title": "Enter Username and Password" + } + }, + "title": "VeSync" } } \ No newline at end of file diff --git a/homeassistant/components/vesync/strings.json b/homeassistant/components/vesync/strings.json index f28d7d0d0c2..2d808e85bea 100644 --- a/homeassistant/components/vesync/strings.json +++ b/homeassistant/components/vesync/strings.json @@ -1,20 +1,20 @@ { - "config": { - "title": "VeSync", - "step": { - "user": { - "title": "Enter Username and Password", - "data": { - "username": "Email Address", - "password": "Password" + "config": { + "title": "VeSync", + "step": { + "user": { + "title": "Enter Username and Password", + "data": { + "username": "Email Address", + "password": "Password" + } + } + }, + "error": { + "invalid_login": "Invalid username or password" + }, + "abort": { + "already_setup": "Only one Vesync instance is allowed" } - } - }, - "error": { - "invalid_login": "Invalid username or password" - }, - "abort": { - "already_setup": "Only one Vesync instance is allowed" } - } } \ No newline at end of file diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index aacfd2a0d96..e1ed6a678e3 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -1,21 +1,20 @@ { - "config": { - "title": "ZHA", - "step": { - "user": { + "config": { "title": "ZHA", - "description": "", - "data": { - "usb_path": "USB Device Path", - "radio_type": "Radio Type" + "step": { + "user": { + "title": "ZHA", + "data": { + "radio_type": "Radio Type", + "usb_path": "USB Device Path" + } + } + }, + "error": { + "cannot_connect": "Unable to connect to ZHA device." + }, + "abort": { + "single_instance_allowed": "Only a single configuration of ZHA is allowed." } - } - }, - "abort": { - "single_instance_allowed": "Only a single configuration of ZHA is allowed." - }, - "error": { - "cannot_connect": "Unable to connect to ZHA device." } - } -} +} \ No newline at end of file From 87119472a37381399ee18f42625271e14cccd345 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 11 Aug 2019 20:42:50 -0700 Subject: [PATCH 116/273] Update translations --- .../components/adguard/.translations/fr.json | 3 ++- .../deconz/.translations/zh-Hant.json | 6 +++--- .../homekit_controller/.translations/no.json | 2 +- .../components/hue/.translations/cy.json | 4 ++-- .../components/hue/.translations/fr.json | 1 + .../components/life360/.translations/fr.json | 6 ++++-- .../components/met/.translations/fr.json | 4 +++- .../components/notion/.translations/cy.json | 14 ++++++++++++++ .../onboarding/.translations/vi.json | 7 +++++++ .../components/plaato/.translations/fr.json | 7 +++++++ .../components/ps4/.translations/no.json | 2 +- .../components/somfy/.translations/fr.json | 7 +------ .../components/tradfri/.translations/fr.json | 3 ++- .../components/tradfri/.translations/no.json | 2 +- .../components/upnp/.translations/da.json | 4 ++++ .../components/upnp/.translations/de.json | 4 ++++ .../components/upnp/.translations/es.json | 4 ++++ .../components/upnp/.translations/hu.json | 4 ++++ .../components/upnp/.translations/lb.json | 4 ++++ .../components/upnp/.translations/nl.json | 4 ++++ .../components/upnp/.translations/nn.json | 4 ++++ .../components/upnp/.translations/no.json | 8 ++++++++ .../components/upnp/.translations/pl.json | 6 ++++++ .../components/upnp/.translations/pt.json | 4 ++++ .../components/upnp/.translations/ro.json | 5 +++++ .../components/upnp/.translations/sl.json | 6 ++++++ .../components/upnp/.translations/sv.json | 4 ++++ .../components/wemo/.translations/fr.json | 10 ++++++++++ .../components/wwlln/.translations/cy.json | 18 ++++++++++++++++++ 29 files changed, 138 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/notion/.translations/cy.json create mode 100644 homeassistant/components/onboarding/.translations/vi.json create mode 100644 homeassistant/components/plaato/.translations/fr.json create mode 100644 homeassistant/components/wemo/.translations/fr.json create mode 100644 homeassistant/components/wwlln/.translations/cy.json diff --git a/homeassistant/components/adguard/.translations/fr.json b/homeassistant/components/adguard/.translations/fr.json index 338a5a77dad..7a58c396345 100644 --- a/homeassistant/components/adguard/.translations/fr.json +++ b/homeassistant/components/adguard/.translations/fr.json @@ -19,6 +19,7 @@ "username": "Nom d'utilisateur" } } - } + }, + "title": "AdGuard Home" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index fbe422bf927..0173c90c3b7 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -25,10 +25,10 @@ "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" }, - "title": "\u5b9a\u7fa9 deCONZ \u7db2\u95dc" + "title": "\u5b9a\u7fa9 deCONZ \u9598\u9053\u5668" }, "link": { - "description": "\u89e3\u9664 deCONZ \u7db2\u95dc\u9396\u5b9a\uff0c\u4ee5\u65bc Home Assistant \u9032\u884c\u8a3b\u518a\u3002\n\n1. \u9032\u5165 deCONZ \u7cfb\u7d71\u8a2d\u5b9a -> \u7db2\u95dc -> \u9032\u968e\u8a2d\u5b9a\n2. \u6309\u4e0b\u300c\u8a8d\u8b49\u7a0b\u5f0f\uff08Authenticate app\uff09\u300d\u6309\u9215", + "description": "\u89e3\u9664 deCONZ \u9598\u9053\u5668\u9396\u5b9a\uff0c\u4ee5\u65bc Home Assistant \u9032\u884c\u8a3b\u518a\u3002\n\n1. \u9032\u5165 deCONZ \u7cfb\u7d71\u8a2d\u5b9a -> \u9598\u9053\u5668 -> \u9032\u968e\u8a2d\u5b9a\n2. \u6309\u4e0b\u300c\u8a8d\u8b49\u7a0b\u5f0f\uff08Authenticate app\uff09\u300d\u6309\u9215", "title": "\u9023\u7d50\u81f3 deCONZ" }, "options": { @@ -39,6 +39,6 @@ "title": "deCONZ \u9644\u52a0\u8a2d\u5b9a\u9078\u9805" } }, - "title": "deCONZ Zigbee \u7db2\u95dc" + "title": "deCONZ Zigbee \u9598\u9053\u5668" } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/no.json b/homeassistant/components/homekit_controller/.translations/no.json index 8dd293dc7c8..db8b8b035e0 100644 --- a/homeassistant/components/homekit_controller/.translations/no.json +++ b/homeassistant/components/homekit_controller/.translations/no.json @@ -24,7 +24,7 @@ "data": { "pairing_code": "Sammenkoblingskode" }, - "description": "Skriv inn HomeKit sammenkoblingskoden for \u00e5 bruke dette tilbeh\u00f8ret", + "description": "Skriv inn din HomeKit-sammenkoblingskode (i formatet XXX-XX-XXX) for \u00e5 bruke dette tilbeh\u00f8ret", "title": "Koble til HomeKit tilbeh\u00f8r" }, "user": { diff --git a/homeassistant/components/hue/.translations/cy.json b/homeassistant/components/hue/.translations/cy.json index 282dd371499..f5476f73edb 100644 --- a/homeassistant/components/hue/.translations/cy.json +++ b/homeassistant/components/hue/.translations/cy.json @@ -24,6 +24,6 @@ "title": "Hwb cyswllt" } }, - "title": "Pont Philips Hue" + "title": "Pont Phillips Hue" } -} +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/fr.json b/homeassistant/components/hue/.translations/fr.json index ddb647c18ed..55b308b2373 100644 --- a/homeassistant/components/hue/.translations/fr.json +++ b/homeassistant/components/hue/.translations/fr.json @@ -7,6 +7,7 @@ "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", + "not_hue_bridge": "Pas de pont Hue", "unknown": "Une erreur inconnue s'est produite" }, "error": { diff --git a/homeassistant/components/life360/.translations/fr.json b/homeassistant/components/life360/.translations/fr.json index 8bb298a0a84..95df1c991a2 100644 --- a/homeassistant/components/life360/.translations/fr.json +++ b/homeassistant/components/life360/.translations/fr.json @@ -14,8 +14,10 @@ "data": { "password": "Mot de passe", "username": "Nom d'utilisateur" - } + }, + "title": "Informations sur le compte Life360" } - } + }, + "title": "Life360" } } \ No newline at end of file diff --git a/homeassistant/components/met/.translations/fr.json b/homeassistant/components/met/.translations/fr.json index d9996c8308c..7100cb5e4a7 100644 --- a/homeassistant/components/met/.translations/fr.json +++ b/homeassistant/components/met/.translations/fr.json @@ -11,8 +11,10 @@ "longitude": "Longitude", "name": "Nom" }, + "description": "Institut m\u00e9t\u00e9orologique", "title": "Emplacement" } - } + }, + "title": "Met.no" } } \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/cy.json b/homeassistant/components/notion/.translations/cy.json new file mode 100644 index 00000000000..63b1c613505 --- /dev/null +++ b/homeassistant/components/notion/.translations/cy.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Cyfrinair", + "username": "Enw Defnyddiwr / Cyfeiriad E-bost" + }, + "title": "Llenwch eich gwybodaeth" + } + }, + "title": "Syniad" + } +} \ No newline at end of file diff --git a/homeassistant/components/onboarding/.translations/vi.json b/homeassistant/components/onboarding/.translations/vi.json new file mode 100644 index 00000000000..652307f69e1 --- /dev/null +++ b/homeassistant/components/onboarding/.translations/vi.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "Ph\u00f2ng ng\u1ee7", + "kitchen": "Ph\u00f2ng b\u1ebfp", + "living_room": "Ph\u00f2ng kh\u00e1ch" + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/.translations/fr.json b/homeassistant/components/plaato/.translations/fr.json new file mode 100644 index 00000000000..091c680be4c --- /dev/null +++ b/homeassistant/components/plaato/.translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Une seule instance est n\u00e9cessaire." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/no.json b/homeassistant/components/ps4/.translations/no.json index 132ec5b83ec..3608a5534ab 100644 --- a/homeassistant/components/ps4/.translations/no.json +++ b/homeassistant/components/ps4/.translations/no.json @@ -25,7 +25,7 @@ "name": "Navn", "region": "Region" }, - "description": "Skriv inn PlayStation 4 informasjonen din. For 'PIN', naviger til 'Innstillinger' p\u00e5 PlayStation 4 konsollen, deretter navigerer du til 'Innstillinger for mobilapp forbindelse' og velger 'Legg til enhet'. Skriv inn PIN-koden som vises.", + "description": "Skriv inn PlayStation 4-informasjonen. For 'PIN', naviger til 'Innstillinger' p\u00e5 PlayStation 4-konsollen. Naviger deretter til 'Mobile App Connection Settings' og velg 'Add Device'. Tast inn PIN-koden som vises. Se [dokumentasjonen] (https://www.home-assistant.io/components/ps4/) for mer informasjon.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/somfy/.translations/fr.json b/homeassistant/components/somfy/.translations/fr.json index 6367e411552..6afb01169cb 100644 --- a/homeassistant/components/somfy/.translations/fr.json +++ b/homeassistant/components/somfy/.translations/fr.json @@ -1,12 +1,7 @@ { "config": { - "abort": { - "already_setup": "Vous ne pouvez configurer qu'un seul compte Somfy.", - "authorize_url_timeout": "Durée expirée pour la génération de l'url d'autorisation.", - "missing_configuration": "Le composant Somfy n'est pas configuré. Merci de suivre la documentation." - }, "create_entry": { - "default": "Authentification réussie avec Somfy." + "default": "Authentifi\u00e9 avec succ\u00e8s avec Somfy." }, "title": "Somfy" } diff --git a/homeassistant/components/tradfri/.translations/fr.json b/homeassistant/components/tradfri/.translations/fr.json index 4fefee631c9..c1dc31028a8 100644 --- a/homeassistant/components/tradfri/.translations/fr.json +++ b/homeassistant/components/tradfri/.translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Le pont est d\u00e9j\u00e0 configur\u00e9." + "already_configured": "Le pont est d\u00e9j\u00e0 configur\u00e9.", + "already_in_progress": "La configuration du pont est d\u00e9j\u00e0 en cours." }, "error": { "cannot_connect": "Impossible de se connecter \u00e0 la passerelle.", diff --git a/homeassistant/components/tradfri/.translations/no.json b/homeassistant/components/tradfri/.translations/no.json index 1448757ca5a..490fbaed5aa 100644 --- a/homeassistant/components/tradfri/.translations/no.json +++ b/homeassistant/components/tradfri/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Bridge er allerede konfigurert", + "already_configured": "Bridge er allerede konfigurert.", "already_in_progress": "Brokonfigurasjon er allerede i gang." }, "error": { diff --git a/homeassistant/components/upnp/.translations/da.json b/homeassistant/components/upnp/.translations/da.json index 778834d24eb..1d0097c2f1f 100644 --- a/homeassistant/components/upnp/.translations/da.json +++ b/homeassistant/components/upnp/.translations/da.json @@ -8,6 +8,10 @@ "no_sensors_or_port_mapping": "Aktiv\u00e9r enten sensorer eller porttilknytning", "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af UPnP/IGD." }, + "error": { + "one": "En", + "other": "Anden" + }, "step": { "confirm": { "description": "Er du sikker p\u00e5 at du vil konfigurere UPnP/IGD?", diff --git a/homeassistant/components/upnp/.translations/de.json b/homeassistant/components/upnp/.translations/de.json index 51faf56367d..907bfffbeea 100644 --- a/homeassistant/components/upnp/.translations/de.json +++ b/homeassistant/components/upnp/.translations/de.json @@ -8,6 +8,10 @@ "no_sensors_or_port_mapping": "Aktiviere mindestens Sensoren oder Port-Mapping", "single_instance_allowed": "Es ist nur eine einzige Konfiguration von UPnP/IGD erforderlich." }, + "error": { + "one": "Ein", + "other": "andere" + }, "step": { "confirm": { "description": "M\u00f6chten Sie UPnP/IGD einrichten?", diff --git a/homeassistant/components/upnp/.translations/es.json b/homeassistant/components/upnp/.translations/es.json index 99b1065fb89..fa299cc379f 100644 --- a/homeassistant/components/upnp/.translations/es.json +++ b/homeassistant/components/upnp/.translations/es.json @@ -8,6 +8,10 @@ "no_sensors_or_port_mapping": "Habilitar al menos sensores o mapeo de puertos", "single_instance_allowed": "S\u00f3lo se necesita una configuraci\u00f3n de UPnP/IGD." }, + "error": { + "one": "UNO", + "other": "OTRO" + }, "step": { "confirm": { "description": "\u00bfDesea configurar UPnP/IGD?", diff --git a/homeassistant/components/upnp/.translations/hu.json b/homeassistant/components/upnp/.translations/hu.json index b4785f2f625..29dab5e09da 100644 --- a/homeassistant/components/upnp/.translations/hu.json +++ b/homeassistant/components/upnp/.translations/hu.json @@ -7,6 +7,10 @@ "no_devices_found": "Nincsenek UPnPIGD eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton.", "single_instance_allowed": "Csak egy UPnP / IGD konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges." }, + "error": { + "one": "hiba", + "other": "" + }, "step": { "confirm": { "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a UPnP/IGD-t?", diff --git a/homeassistant/components/upnp/.translations/lb.json b/homeassistant/components/upnp/.translations/lb.json index 55d41e588f6..029e1e87cf1 100644 --- a/homeassistant/components/upnp/.translations/lb.json +++ b/homeassistant/components/upnp/.translations/lb.json @@ -8,6 +8,10 @@ "no_sensors_or_port_mapping": "Aktiv\u00e9ier op mannst Sensoren oder Port Mapping", "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun UPnP/IGD ass n\u00e9ideg." }, + "error": { + "one": "Een", + "other": "Aaner" + }, "step": { "confirm": { "description": "Soll UPnP/IGD konfigur\u00e9iert ginn?", diff --git a/homeassistant/components/upnp/.translations/nl.json b/homeassistant/components/upnp/.translations/nl.json index 5d426f2edaf..a94471bb610 100644 --- a/homeassistant/components/upnp/.translations/nl.json +++ b/homeassistant/components/upnp/.translations/nl.json @@ -8,6 +8,10 @@ "no_sensors_or_port_mapping": "Schakel ten minste sensoren of poorttoewijzing in", "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van UPnP/IGD nodig." }, + "error": { + "one": "Een", + "other": "Ander" + }, "step": { "confirm": { "description": "Wilt u UPnP/IGD instellen?", diff --git a/homeassistant/components/upnp/.translations/nn.json b/homeassistant/components/upnp/.translations/nn.json index 63339244116..286efcf0353 100644 --- a/homeassistant/components/upnp/.translations/nn.json +++ b/homeassistant/components/upnp/.translations/nn.json @@ -3,6 +3,10 @@ "abort": { "no_sensors_or_port_mapping": "I det minste, aktiver sensor eller portkartlegging" }, + "error": { + "one": "Ein", + "other": "Andre" + }, "step": { "init": { "title": "UPnP / IGD" diff --git a/homeassistant/components/upnp/.translations/no.json b/homeassistant/components/upnp/.translations/no.json index b3b6ef17366..813509121e3 100644 --- a/homeassistant/components/upnp/.translations/no.json +++ b/homeassistant/components/upnp/.translations/no.json @@ -8,6 +8,14 @@ "no_sensors_or_port_mapping": "Aktiver minst sensorer eller port mapping", "single_instance_allowed": "Bare en enkelt konfigurasjon av UPnP / IGD er n\u00f8dvendig." }, + "error": { + "few": "f\u00e5", + "many": "mange", + "one": "en", + "other": "andre", + "two": "to", + "zero": "ingen" + }, "step": { "confirm": { "description": "\u00d8nsker du \u00e5 konfigurere UPnP / IGD?", diff --git a/homeassistant/components/upnp/.translations/pl.json b/homeassistant/components/upnp/.translations/pl.json index 94823bd90d3..d7ede44d22d 100644 --- a/homeassistant/components/upnp/.translations/pl.json +++ b/homeassistant/components/upnp/.translations/pl.json @@ -8,6 +8,12 @@ "no_sensors_or_port_mapping": "W\u0142\u0105cz przynajmniej sensory lub mapowanie port\u00f3w", "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja UPnP/IGD." }, + "error": { + "few": "kilka", + "many": "wiele", + "one": "jeden", + "other": "inne" + }, "step": { "confirm": { "description": "Czy chcesz skonfigurowa\u0107 UPnP/IGD?", diff --git a/homeassistant/components/upnp/.translations/pt.json b/homeassistant/components/upnp/.translations/pt.json index 698a0d98f3d..d559a05ff23 100644 --- a/homeassistant/components/upnp/.translations/pt.json +++ b/homeassistant/components/upnp/.translations/pt.json @@ -8,6 +8,10 @@ "no_sensors_or_port_mapping": "Ative pelo menos os sensores ou o mapeamento de porta", "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do UPnP/IGD \u00e9 necess\u00e1ria." }, + "error": { + "one": "um", + "other": "v\u00e1rios" + }, "step": { "confirm": { "description": "Deseja configurar o UPnP / IGD?", diff --git a/homeassistant/components/upnp/.translations/ro.json b/homeassistant/components/upnp/.translations/ro.json index 1a328adb9ef..bb584da05dc 100644 --- a/homeassistant/components/upnp/.translations/ro.json +++ b/homeassistant/components/upnp/.translations/ro.json @@ -4,6 +4,11 @@ "already_configured": "UPnP/IGD este deja configurat", "no_devices_discovered": "Nu au fost descoperite UPnP/IGD-uri" }, + "error": { + "few": "", + "one": "Unul", + "other": "" + }, "step": { "init": { "title": "UPnP/IGD" diff --git a/homeassistant/components/upnp/.translations/sl.json b/homeassistant/components/upnp/.translations/sl.json index b31a7e792ea..4c019d8f207 100644 --- a/homeassistant/components/upnp/.translations/sl.json +++ b/homeassistant/components/upnp/.translations/sl.json @@ -8,6 +8,12 @@ "no_sensors_or_port_mapping": "Omogo\u010dite vsaj senzorje ali preslikavo vrat (port mapping)", "single_instance_allowed": "Potrebna je samo ena konfiguracija UPnp/IGD." }, + "error": { + "few": "nekaj", + "one": "ena", + "other": "ve\u010d", + "two": "dve" + }, "step": { "confirm": { "description": "Ali \u017eelite nastaviti UPnp/IGD?", diff --git a/homeassistant/components/upnp/.translations/sv.json b/homeassistant/components/upnp/.translations/sv.json index c2447be1216..e3864aee4da 100644 --- a/homeassistant/components/upnp/.translations/sv.json +++ b/homeassistant/components/upnp/.translations/sv.json @@ -8,6 +8,10 @@ "no_sensors_or_port_mapping": "Aktivera minst sensorer eller portmappning", "single_instance_allowed": "Endast en enda konfiguration av UPnP/IGD \u00e4r n\u00f6dv\u00e4ndig." }, + "error": { + "one": "En", + "other": "Andra" + }, "step": { "confirm": { "description": "Vill du konfigurera UPnP/IGD?", diff --git a/homeassistant/components/wemo/.translations/fr.json b/homeassistant/components/wemo/.translations/fr.json new file mode 100644 index 00000000000..c1c8830cb25 --- /dev/null +++ b/homeassistant/components/wemo/.translations/fr.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "Wemo" + } + }, + "title": "Wemo" + } +} \ No newline at end of file diff --git a/homeassistant/components/wwlln/.translations/cy.json b/homeassistant/components/wwlln/.translations/cy.json new file mode 100644 index 00000000000..e9de2acbdc6 --- /dev/null +++ b/homeassistant/components/wwlln/.translations/cy.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Enw eisoes wedi gofrestru" + }, + "step": { + "user": { + "data": { + "latitude": "Lledred", + "longitude": "Hydred", + "radius": "Radiws (gan ddefnyddio'ch system uned sylfaenol)" + }, + "title": "Cwblhewch gwybodaeth eich lleoliad" + } + }, + "title": "Rhwydwaith Lleoliad Golau Byd-eang (WWLLN)" + } +} \ No newline at end of file From af70b6da20d0b043f62a1bd27044e505ba45e068 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 12 Aug 2019 05:48:56 +0200 Subject: [PATCH 117/273] Fix issue with nuki new available state (#25881) --- homeassistant/components/nuki/lock.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 38e42fcc1b5..31a655dfedd 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -144,10 +144,12 @@ class NukiLock(LockDevice): self._nuki_lock.update(aggressive=False) except requests.exceptions.RequestException: self._available = False - else: - self._name = self._nuki_lock.name - self._locked = self._nuki_lock.is_locked - self._battery_critical = self._nuki_lock.battery_critical + return + + self._available = self._nuki_lock.state != 255 + self._name = self._nuki_lock.name + self._locked = self._nuki_lock.is_locked + self._battery_critical = self._nuki_lock.battery_critical def lock(self, **kwargs): """Lock the device.""" From 5f0334d2084f2ab5cd7210adeb5776c06890559c Mon Sep 17 00:00:00 2001 From: ejaviga <52974243+ejaviga@users.noreply.github.com> Date: Mon, 12 Aug 2019 06:02:16 +0200 Subject: [PATCH 118/273] Tado AIR_CONDITIONING module was not working propertly (#25677) * Tado AIR_CONDITIONING module was not working propertly AIR_CONDITIONING modules differs from HEATING module int he parameters. * Tado Cooling Sensor was not read proprtly * TADO correct file permissions * Tado: Fix compilation error * Fix Lint errors * Fix Black formatting * TADO More AC functionality Also Black formatting * Tado Fix Lint * Tado Fix Lint II --- homeassistant/components/tado/__init__.py | 18 +- homeassistant/components/tado/climate.py | 251 ++++++++++++++++++---- homeassistant/components/tado/sensor.py | 44 +++- 3 files changed, 264 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 519c1ced416..ad66a594a86 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -125,12 +125,22 @@ class TadoDataStore: self.tado.resetZoneOverlay(zone_id) self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg - def set_zone_overlay(self, zone_id, mode, temperature=None, duration=None): + def set_zone_overlay( + self, + zone_id, + overlay_mode, + temperature=None, + duration=None, + device_type="HEATING", + mode=None, + ): """Wrap for setZoneOverlay(..).""" - self.tado.setZoneOverlay(zone_id, mode, temperature, duration) + self.tado.setZoneOverlay( + zone_id, overlay_mode, temperature, duration, device_type, "ON", mode + ) self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg - def set_zone_off(self, zone_id, mode): + def set_zone_off(self, zone_id, overlay_mode, device_type="HEATING"): """Set a zone to off.""" - self.tado.setZoneOverlay(zone_id, mode, None, None, "HEATING", "OFF") + self.tado.setZoneOverlay(zone_id, overlay_mode, None, None, device_type, "OFF") self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 1d310ef7c16..15e01db4082 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -1,18 +1,24 @@ """Support for Tado to create a climate device for each zone.""" import logging +from typing import Optional, List from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_OFF, CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, FAN_HIGH, FAN_LOW, FAN_MIDDLE, FAN_OFF, HVAC_MODE_AUTO, HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_AWAY, + PRESET_HOME, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -40,18 +46,34 @@ CONST_MODE_FAN_LOW = "LOW" FAN_MAP_TADO = {"HIGH": FAN_HIGH, "MIDDLE": FAN_MIDDLE, "LOW": FAN_LOW} -HVAC_MAP_TADO = { +HVAC_MAP_TADO_HEAT = { "MANUAL": HVAC_MODE_HEAT, "TIMER": HVAC_MODE_AUTO, "TADO_MODE": HVAC_MODE_AUTO, "SMART_SCHEDULE": HVAC_MODE_AUTO, "OFF": HVAC_MODE_OFF, } +HVAC_MAP_TADO_COOL = { + "MANUAL": HVAC_MODE_COOL, + "TIMER": HVAC_MODE_AUTO, + "TADO_MODE": HVAC_MODE_AUTO, + "SMART_SCHEDULE": HVAC_MODE_AUTO, + "OFF": HVAC_MODE_OFF, +} +HVAC_MAP_TADO_HEAT_COOL = { + "MANUAL": HVAC_MODE_HEAT_COOL, + "TIMER": HVAC_MODE_AUTO, + "TADO_MODE": HVAC_MODE_AUTO, + "SMART_SCHEDULE": HVAC_MODE_AUTO, + "OFF": HVAC_MODE_OFF, +} SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE -SUPPORT_HVAC = [HVAC_MODE_HEAT, HVAC_MODE_AUTO, HVAC_MODE_OFF] -SUPPORT_FAN = [FAN_HIGH, FAN_MIDDLE, FAN_HIGH, FAN_OFF] -SUPPORT_PRESET = [PRESET_AWAY] +SUPPORT_HVAC_HEAT = [HVAC_MODE_HEAT, HVAC_MODE_AUTO, HVAC_MODE_OFF] +SUPPORT_HVAC_COOL = [HVAC_MODE_COOL, HVAC_MODE_AUTO, HVAC_MODE_OFF] +SUPPORT_HVAC_HEAT_COOL = [HVAC_MODE_HEAT_COOL, HVAC_MODE_AUTO, HVAC_MODE_OFF] +SUPPORT_FAN = [FAN_HIGH, FAN_MIDDLE, FAN_LOW, FAN_OFF] +SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME] def setup_platform(hass, config, add_entities, discovery_info=None): @@ -80,10 +102,18 @@ def create_climate_device(tado, hass, zone, name, zone_id): capabilities = tado.get_capabilities(zone_id) unit = TEMP_CELSIUS - ac_mode = capabilities["type"] == "AIR_CONDITIONING" + ac_device = capabilities["type"] == "AIR_CONDITIONING" + ac_support_heat = False - if ac_mode: - temperatures = capabilities["HEAT"]["temperatures"] + if ac_device: + # Only use heat if available + # (you don't have to setup a heat mode, but cool is required) + # Heat is preferred as it generally has a lower minimum temperature + if "HEAT" in capabilities: + temperatures = capabilities["HEAT"]["temperatures"] + ac_support_heat = True + else: + temperatures = capabilities["COOL"]["temperatures"] elif "temperatures" in capabilities: temperatures = capabilities["temperatures"] else: @@ -103,7 +133,8 @@ def create_climate_device(tado, hass, zone, name, zone_id): hass.config.units.temperature(min_temp, unit), hass.config.units.temperature(max_temp, unit), step, - ac_mode, + ac_device, + ac_support_heat, ) tado.add_sensor( @@ -114,7 +145,7 @@ def create_climate_device(tado, hass, zone, name, zone_id): class TadoClimate(ClimateDevice): - """Representation of a tado climate device.""" + """Representation of a Tado climate device.""" def __init__( self, @@ -125,7 +156,8 @@ class TadoClimate(ClimateDevice): min_temp, max_temp, step, - ac_mode, + ac_device, + ac_support_heat, tolerance=0.3, ): """Initialize of Tado climate device.""" @@ -135,7 +167,9 @@ class TadoClimate(ClimateDevice): self.zone_name = zone_name self.zone_id = zone_id - self.ac_mode = ac_mode + self._ac_device = ac_device + self._ac_support_heat = ac_support_heat + self._cooling = False self._active = False self._device_is_active = False @@ -149,7 +183,6 @@ class TadoClimate(ClimateDevice): self._step = step self._target_temp = None self._tolerance = tolerance - self._cooling = False self._current_fan = CONST_MODE_OFF self._current_operation = CONST_MODE_SMART_SCHEDULE @@ -170,6 +203,10 @@ class TadoClimate(ClimateDevice): """Return the current humidity.""" return self._cur_humidity + def set_humidity(self, humidity: int) -> None: + """Set new target humidity.""" + pass + @property def current_temperature(self): """Return the sensor temperature.""" @@ -181,7 +218,11 @@ class TadoClimate(ClimateDevice): Need to be one of HVAC_MODE_*. """ - return HVAC_MAP_TADO.get(self._current_operation) + if self._ac_device and self._ac_support_heat: + return HVAC_MAP_TADO_HEAT_COOL.get(self._current_operation) + if self._ac_device and not self._ac_support_heat: + return HVAC_MAP_TADO_COOL.get(self._current_operation) + return HVAC_MAP_TADO_HEAT.get(self._current_operation) @property def hvac_modes(self): @@ -189,7 +230,11 @@ class TadoClimate(ClimateDevice): Need to be a subset of HVAC_MODES. """ - return SUPPORT_HVAC + if self._ac_device and self._ac_support_heat: + return SUPPORT_HVAC_HEAT_COOL + if self._ac_device and not self._ac_support_heat: + return SUPPORT_HVAC_COOL + return SUPPORT_HVAC_HEAT @property def hvac_action(self): @@ -197,36 +242,58 @@ class TadoClimate(ClimateDevice): Need to be one of CURRENT_HVAC_*. """ - if self._cooling: - return CURRENT_HVAC_COOL - return CURRENT_HVAC_HEAT + if not self._device_is_active: + return CURRENT_HVAC_OFF + if self._ac_device and self._ac_support_heat and self._cooling: + if self._active: + return CURRENT_HVAC_COOL + return CURRENT_HVAC_IDLE + if self._ac_device and self._ac_support_heat and not self._cooling: + if self._active: + return CURRENT_HVAC_HEAT + return CURRENT_HVAC_IDLE + if self._ac_device and not self._ac_support_heat: + if self._active: + return CURRENT_HVAC_COOL + return CURRENT_HVAC_IDLE + if self._active: + return CURRENT_HVAC_HEAT + return CURRENT_HVAC_IDLE @property def fan_mode(self): """Return the fan setting.""" - if self.ac_mode: + if self._ac_device: return FAN_MAP_TADO.get(self._current_fan) return None @property def fan_modes(self): """List of available fan modes.""" - if self.ac_mode: + if self._ac_device: return SUPPORT_FAN return None + def set_fan_mode(self, fan_mode: str): + """Turn fan on/off.""" + pass + @property def preset_mode(self): """Return the current preset mode, e.g., home, away, temp.""" if self._is_away: return PRESET_AWAY - return None + return PRESET_HOME @property def preset_modes(self): """Return a list of available preset modes.""" return SUPPORT_PRESET + def set_preset_mode(self, preset_mode): + """Set new preset mode.""" + pass + @property def temperature_unit(self): """Return the unit of measurement used by the platform.""" @@ -242,13 +309,23 @@ class TadoClimate(ClimateDevice): """Return the temperature we try to reach.""" return self._target_temp + @property + def target_temperature_high(self): + """Return the upper bound temperature we try to reach.""" + return None + + @property + def target_temperature_low(self): + """Return the lower bound temperature we try to reach.""" + return None + def set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: return - self._current_operation = CONST_OVERLAY_TADO_MODE + self._current_operation = CONST_OVERLAY_MANUAL self._overlay_mode = None self._target_temp = temperature self._control_heating() @@ -263,15 +340,17 @@ class TadoClimate(ClimateDevice): mode = CONST_MODE_SMART_SCHEDULE elif hvac_mode == HVAC_MODE_HEAT: mode = CONST_OVERLAY_MANUAL + elif hvac_mode == HVAC_MODE_COOL: + mode = CONST_OVERLAY_MANUAL + elif hvac_mode == HVAC_MODE_HEAT_COOL: + mode = CONST_OVERLAY_MANUAL self._current_operation = mode self._overlay_mode = None + if self._target_temp is None and self._ac_device: + self._target_temp = 27 self._control_heating() - def set_preset_mode(self, preset_mode): - """Set new preset mode.""" - pass - @property def min_temp(self): """Return the minimum temperature.""" @@ -333,6 +412,22 @@ class TadoClimate(ClimateDevice): else: self._device_is_active = True + active = False + if "activityDataPoints" in data: + activity_data = data["activityDataPoints"] + if self._ac_device: + if "acPower" in activity_data and activity_data["acPower"] is not None: + if not activity_data["acPower"]["value"] == "OFF": + active = True + else: + if ( + "heatingPower" in activity_data + and activity_data["heatingPower"] is not None + ): + if float(activity_data["heatingPower"]["percentage"]) > 0.0: + active = True + self._active = active + overlay = False overlay_data = None termination = CONST_MODE_SMART_SCHEDULE @@ -345,6 +440,8 @@ class TadoClimate(ClimateDevice): if overlay: termination = overlay_data["termination"]["type"] + setting = False + setting_data = None if "setting" in overlay_data: setting_data = overlay_data["setting"] @@ -368,34 +465,110 @@ class TadoClimate(ClimateDevice): def _control_heating(self): """Send new target temperature to mytado.""" - if not self._active and None not in (self._cur_temp, self._target_temp): - self._active = True + if None not in (self._cur_temp, self._target_temp): _LOGGER.info( - "Obtained current and target temperature. " "Tado thermostat active" + "Obtained current (%d) and target temperature (%d). " + "Tado thermostat active", + self._cur_temp, + self._target_temp, ) if self._current_operation == CONST_MODE_SMART_SCHEDULE: _LOGGER.info( - "Switching mytado.com to SCHEDULE (default) " "for zone %s", + "Switching mytado.com to SCHEDULE (default) for zone %s (%d)", self.zone_name, + self.zone_id, ) self._store.reset_zone_overlay(self.zone_id) self._overlay_mode = self._current_operation return if self._current_operation == CONST_MODE_OFF: - _LOGGER.info("Switching mytado.com to OFF for zone %s", self.zone_name) - self._store.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL) + if self._ac_device: + _LOGGER.info( + "Switching mytado.com to OFF for zone %s (%d) - AIR_CONDITIONING", + self.zone_name, + self.zone_id, + ) + self._store.set_zone_off( + self.zone_id, CONST_OVERLAY_MANUAL, "AIR_CONDITIONING" + ) + else: + _LOGGER.info( + "Switching mytado.com to OFF for zone %s (%d) - HEATING", + self.zone_name, + self.zone_id, + ) + self._store.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, "HEATING") self._overlay_mode = self._current_operation return - _LOGGER.info( - "Switching mytado.com to %s mode for zone %s", - self._current_operation, - self.zone_name, - ) - self._store.set_zone_overlay( - self.zone_id, self._current_operation, self._target_temp - ) + if self._ac_device: + _LOGGER.info( + "Switching mytado.com to %s mode for zone %s (%d). Temp (%s) - AIR_CONDITIONING", + self._current_operation, + self.zone_name, + self.zone_id, + self._target_temp, + ) + self._store.set_zone_overlay( + self.zone_id, + self._current_operation, + self._target_temp, + None, + "AIR_CONDITIONING", + "COOL", + ) + else: + _LOGGER.info( + "Switching mytado.com to %s mode for zone %s (%d). Temp (%s) - HEATING", + self._current_operation, + self.zone_name, + self.zone_id, + self._target_temp, + ) + self._store.set_zone_overlay( + self.zone_id, + self._current_operation, + self._target_temp, + None, + "HEATING", + ) self._overlay_mode = self._current_operation + + @property + def is_aux_heat(self) -> Optional[bool]: + """Return true if aux heater. + + Requires SUPPORT_AUX_HEAT. + """ + return None + + def turn_aux_heat_on(self) -> None: + """Turn auxiliary heater on.""" + pass + + def turn_aux_heat_off(self) -> None: + """Turn auxiliary heater off.""" + pass + + @property + def swing_mode(self) -> Optional[str]: + """Return the swing setting. + + Requires SUPPORT_SWING_MODE. + """ + return None + + @property + def swing_modes(self) -> Optional[List[str]]: + """Return the list of available swing modes. + + Requires SUPPORT_SWING_MODE. + """ + return None + + def set_swing_mode(self, swing_mode: str) -> None: + """Set new target swing operation.""" + pass diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index edabdaebfdc..5cfdbd1f30c 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -12,7 +12,7 @@ ATTR_DATA_ID = "data_id" ATTR_DEVICE = "device" ATTR_ZONE = "zone" -CLIMATE_SENSOR_TYPES = [ +CLIMATE_HEAT_SENSOR_TYPES = [ "temperature", "humidity", "power", @@ -22,6 +22,16 @@ CLIMATE_SENSOR_TYPES = [ "overlay", ] +CLIMATE_COOL_SENSOR_TYPES = [ + "temperature", + "humidity", + "power", + "link", + "ac", + "tado mode", + "overlay", +] + HOT_WATER_SENSOR_TYPES = ["power", "link", "tado mode", "overlay"] @@ -38,7 +48,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensor_items = [] for zone in zones: if zone["type"] == "HEATING": - for variable in CLIMATE_SENSOR_TYPES: + for variable in CLIMATE_HEAT_SENSOR_TYPES: sensor_items.append( create_zone_sensor(tado, zone, zone["name"], zone["id"], variable) ) @@ -47,6 +57,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensor_items.append( create_zone_sensor(tado, zone, zone["name"], zone["id"], variable) ) + elif zone["type"] == "AIR_CONDITIONING": + for variable in CLIMATE_COOL_SENSOR_TYPES: + sensor_items.append( + create_zone_sensor(tado, zone, zone["name"], zone["id"], variable) + ) me_data = tado.get_me() sensor_items.append( @@ -138,6 +153,8 @@ class TadoSensor(Entity): return "%" if self.zone_variable == "heating": return "%" + if self.zone_variable == "ac": + return "" @property def icon(self): @@ -198,10 +215,25 @@ class TadoSensor(Entity): elif self.zone_variable == "heating": if "activityDataPoints" in data: activity_data = data["activityDataPoints"] - self._state = float(activity_data["heatingPower"]["percentage"]) - self._state_attributes = { - "time": activity_data["heatingPower"]["timestamp"] - } + + if ( + "heatingPower" in activity_data + and activity_data["heatingPower"] is not None + ): + self._state = float(activity_data["heatingPower"]["percentage"]) + self._state_attributes = { + "time": activity_data["heatingPower"]["timestamp"] + } + + elif self.zone_variable == "ac": + if "activityDataPoints" in data: + activity_data = data["activityDataPoints"] + + if "acPower" in activity_data and activity_data["acPower"] is not None: + self._state = activity_data["acPower"]["value"] + self._state_attributes = { + "time": activity_data["acPower"]["timestamp"] + } elif self.zone_variable == "tado bridge status": if "connectionState" in data: From b38c40fb987f731958d3943888ac962f049847cb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 11 Aug 2019 21:17:32 -0700 Subject: [PATCH 119/273] Updated frontend to 20190811.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 6d25e846db9..9a39e61e727 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==20190805.0" + "home-assistant-frontend==20190811.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a1148063aee..7a43109bf1f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.16 -home-assistant-frontend==20190805.0 +home-assistant-frontend==20190811.0 importlib-metadata==0.18 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index e513dcd6715..e41f16b1d0c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -630,7 +630,7 @@ hole==0.3.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190805.0 +home-assistant-frontend==20190811.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b873eb2e105..6f543f99f4b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -177,7 +177,7 @@ hdate==0.9.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190805.0 +home-assistant-frontend==20190811.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 76f2e6015fab359b79050b0c85b61f5dc64ef1b3 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 12 Aug 2019 11:15:39 +0200 Subject: [PATCH 120/273] Update devcontainer.json --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c85eaece8b6..595506c8ccc 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,7 @@ "postCreateCommand": "pip3 install -e .", "appPort": 8123, "runArgs": [ - "-e", "GIT_EDTIOR='code --wait'" + "-e", "GIT_EDITOR='code --wait'" ], "extensions": [ "ms-python.python", From 6f357cb23eca0a0be2729416dad5ae40c99f36b4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 12 Aug 2019 07:42:12 -0700 Subject: [PATCH 121/273] First pass fixing tests that raise exceptions caught by asyncio.gather (#25860) * First pass fixing tests that raise exceptions caught by asyncio.gather * Fix demo test * lint --- homeassistant/components/demo/__init__.py | 11 ++++++++-- homeassistant/components/roomba/vacuum.py | 2 +- homeassistant/components/vacuum/__init__.py | 20 +++++++++++++++++++ homeassistant/config.py | 3 ++- tests/common.py | 2 ++ tests/components/alexa/test_state_report.py | 2 +- tests/components/axis/test_config_flow.py | 8 ++++++++ tests/components/cloud/test_alexa_config.py | 13 ++++++------ tests/components/deconz/test_binary_sensor.py | 1 + tests/components/deconz/test_climate.py | 1 + tests/components/deconz/test_cover.py | 1 + tests/components/deconz/test_light.py | 1 + tests/components/deconz/test_sensor.py | 1 + tests/components/deconz/test_switch.py | 1 + .../components/homematicip_cloud/test_init.py | 16 +++++++++++++-- .../smartthings/test_config_flow.py | 2 ++ tests/components/smartthings/test_init.py | 3 +++ tests/components/upnp/test_init.py | 5 ++++- 18 files changed, 79 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index f8b61167ef7..967b7852c6f 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -176,8 +176,15 @@ async def async_setup(hass, config): async def finish_setup(hass, config): """Finish set up once demo platforms are set up.""" - lights = sorted(hass.states.async_entity_ids("light")) - switches = sorted(hass.states.async_entity_ids("switch")) + switches = None + lights = None + + while not switches and not lights: + # Not all platforms might be loaded. + if switches is not None: + await asyncio.sleep(0) + switches = sorted(hass.states.async_entity_ids("switch")) + lights = sorted(hass.states.async_entity_ids("light")) # Set up history graph await bootstrap.async_setup_component( diff --git a/homeassistant/components/roomba/vacuum.py b/homeassistant/components/roomba/vacuum.py index ba202c4ad9e..766fd72cdba 100644 --- a/homeassistant/components/roomba/vacuum.py +++ b/homeassistant/components/roomba/vacuum.py @@ -199,7 +199,7 @@ class RoombaVacuum(VacuumDevice): await self.hass.async_add_job(self.vacuum.send_command, "resume") self._is_on = True - async def async_pause(self, **kwargs): + async def async_pause(self): """Pause the cleaning cycle.""" await self.hass.async_add_job(self.vacuum.send_command, "pause") self._is_on = False diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 598c068aad5..9bc376916c6 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -324,6 +324,14 @@ class VacuumDevice(_BaseVacuum, ToggleEntity): """ await self.hass.async_add_executor_job(partial(self.start_pause, **kwargs)) + async def async_pause(self): + """Not supported.""" + pass + + async def async_start(self): + """Not supported.""" + pass + class StateVacuumDevice(_BaseVacuum): """Representation of a vacuum cleaner robot that supports states.""" @@ -378,3 +386,15 @@ class StateVacuumDevice(_BaseVacuum): This method must be run in the event loop. """ await self.hass.async_add_executor_job(self.pause) + + async def async_turn_on(self, **kwargs): + """Not supported.""" + pass + + async def async_turn_off(self, **kwargs): + """Not supported.""" + pass + + async def async_toggle(self, **kwargs): + """Not supported.""" + pass diff --git a/homeassistant/config.py b/homeassistant/config.py index ccf1317e660..1f42b3db25e 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -322,7 +322,8 @@ async def async_hass_config_yaml(hass: HomeAssistant) -> Dict: config = load_yaml_config_file(path) return config - config = await hass.async_add_executor_job(_load_hass_yaml_config) + # Not using async_add_executor_job because this is an internal method. + config = await hass.loop.run_in_executor(None, _load_hass_yaml_config) core_config = config.get(CONF_CORE, {}) await merge_packages_config(hass, config, core_config.get(CONF_PACKAGES, {})) return config diff --git a/tests/common.py b/tests/common.py index 3ca4046db85..a139ca83743 100644 --- a/tests/common.py +++ b/tests/common.py @@ -971,6 +971,8 @@ async def flush_store(store): if store._data is None: return + store._async_cleanup_stop_listener() + store._async_cleanup_delay_listener() await store._async_handle_write_data() diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index 9f745792e59..f6bb4c9cc29 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -5,7 +5,7 @@ from . import TEST_URL, DEFAULT_CONFIG async def test_report_state(hass, aioclient_mock): """Test proactive state reports.""" - aioclient_mock.post(TEST_URL, json={"data": "is irrelevant"}) + aioclient_mock.post(TEST_URL, json={"data": "is irrelevant"}, status=202) hass.states.async_set( "binary_sensor.test_contact", diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index a7d67e39f44..5ec3f933e9e 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -39,6 +39,8 @@ async def test_flow_works(hass): mock_device.side_effect = mock_constructor mock_device.vapix.params.system_serialnumber = "serialnumber" mock_device.vapix.params.prodnbr = "prodnbr" + mock_device.vapix.params.prodtype = "prodtype" + mock_device.vapix.params.firmware_version = "firmware_version" result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} @@ -220,6 +222,10 @@ async def test_zeroconf_flow_known_device(hass): return mock_device mock_device.side_effect = mock_constructor + mock_device.vapix.params.system_serialnumber = "serialnumber" + mock_device.vapix.params.prodnbr = "prodnbr" + mock_device.vapix.params.prodtype = "prodtype" + mock_device.vapix.params.firmware_version = "firmware_version" result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, @@ -338,6 +344,8 @@ async def test_import_flow_works(hass): mock_device.side_effect = mock_constructor mock_device.vapix.params.system_serialnumber = "serialnumber" mock_device.vapix.params.prodnbr = "prodnbr" + mock_device.vapix.params.prodtype = "prodtype" + mock_device.vapix.params.firmware_version = "firmware_version" result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 688d69c16f1..22d8c64c3b0 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -52,16 +52,17 @@ def patch_sync_helper(): to_update = [] to_remove = [] + async def sync_helper(to_upd, to_rem): + to_update.extend([ent_id for ent_id in to_upd if ent_id not in to_update]) + to_remove.extend([ent_id for ent_id in to_rem if ent_id not in to_remove]) + return True + with patch("homeassistant.components.cloud.alexa_config.SYNC_DELAY", 0), patch( "homeassistant.components.cloud.alexa_config.AlexaConfig._sync_helper", - side_effect=mock_coro, - ) as mock_helper: + side_effect=sync_helper, + ): yield to_update, to_remove - actual_to_update, actual_to_remove = mock_helper.mock_calls[0][1] - to_update.extend(actual_to_update) - to_remove.extend(actual_to_remove) - async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs): """Test Alexa config responds to updating exposed entities.""" diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 9eb408ba4f1..9e6d4f571ea 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -115,6 +115,7 @@ async def test_add_new_sensor(hass): sensor.name = "name" sensor.type = "ZHAPresence" sensor.BINARY = True + sensor.uniqueid = "1" sensor.register_async_callback = Mock() async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor]) await hass.async_block_till_done() diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 264c3b8761f..68d1957f97e 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -178,6 +178,7 @@ async def test_add_new_climate_device(hass): sensor = Mock() sensor.name = "name" sensor.type = "ZHAThermostat" + sensor.uniqueid = "1" sensor.register_async_callback = Mock() async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor]) await hass.async_block_till_done() diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index c6f3a4b12c8..ee68744f999 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -132,6 +132,7 @@ async def test_add_new_cover(hass): cover = Mock() cover.name = "name" cover.type = "Level controllable output" + cover.uniqueid = "1" cover.register_async_callback = Mock() async_dispatcher_send(hass, gateway.async_event_new_device("light"), [cover]) await hass.async_block_till_done() diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 77e983e34b4..ee81e476a12 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -190,6 +190,7 @@ async def test_add_new_light(hass): gateway = await setup_gateway(hass, {}) light = Mock() light.name = "name" + light.uniqueid = "1" light.register_async_callback = Mock() async_dispatcher_send(hass, gateway.async_event_new_device("light"), [light]) await hass.async_block_till_done() diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 9c03f3e9a90..21efd768be2 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -158,6 +158,7 @@ async def test_add_new_sensor(hass): sensor = Mock() sensor.name = "name" sensor.type = "ZHATemperature" + sensor.uniqueid = "1" sensor.BINARY = False sensor.register_async_callback = Mock() async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor]) diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 8275e91ecf9..433386a1751 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -140,6 +140,7 @@ async def test_add_new_switch(hass): switch = Mock() switch.name = "name" switch.type = "Smart plug" + switch.uniqueid = "1" switch.register_async_callback = Mock() async_dispatcher_send(hass, gateway.async_event_new_device("light"), [switch]) await hass.async_block_till_done() diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index 5d160108eb5..d77d4a7e5b2 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -68,7 +68,13 @@ async def test_setup_entry_successful(hass): ) entry.add_to_hass(hass) with patch.object(hmipc, "HomematicipHAP") as mock_hap: - mock_hap.return_value.async_setup.return_value = mock_coro(True) + instance = mock_hap.return_value + instance.async_setup.return_value = mock_coro(True) + instance.home.id = "1" + instance.home.modelType = "mock-type" + instance.home.name = "mock-name" + instance.home.currentAPVersion = "mock-ap-version" + assert ( await async_setup_component( hass, @@ -129,7 +135,13 @@ async def test_unload_entry(hass): entry.add_to_hass(hass) with patch.object(hmipc, "HomematicipHAP") as mock_hap: - mock_hap.return_value.async_setup.return_value = mock_coro(True) + instance = mock_hap.return_value + instance.async_setup.return_value = mock_coro(True) + instance.home.id = "1" + instance.home.modelType = "mock-type" + instance.home.name = "mock-name" + instance.home.currentAPVersion = "mock-ap-version" + assert await async_setup_component(hass, hmipc.DOMAIN, {}) is True assert len(mock_hap.return_value.mock_calls) >= 1 diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 0ff4b08c750..5724d7a3bac 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -6,6 +6,7 @@ from asynctest import Mock, patch from pysmartthings import APIResponseError from homeassistant import data_entry_flow +from homeassistant.setup import async_setup_component from homeassistant.components import cloud from homeassistant.components.smartthings import smartapp from homeassistant.components.smartthings.config_flow import SmartThingsFlowHandler @@ -288,6 +289,7 @@ async def test_multiple_config_entry_created_when_installed( hass, app, locations, installed_apps, smartthings_mock ): """Test a config entries are created for multiple installs.""" + assert await async_setup_component(hass, "persistent_notification", {}) flow = SmartThingsFlowHandler() flow.hass = hass flow.access_token = str(uuid4()) diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 881be3b992d..15b556f1d83 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -6,6 +6,7 @@ from asynctest import Mock, patch from pysmartthings import InstalledAppStatus, OAuthToken import pytest +from homeassistant.setup import async_setup_component from homeassistant.components import cloud, smartthings from homeassistant.components.smartthings.const import ( CONF_CLOUDHOOK_URL, @@ -25,6 +26,7 @@ from tests.common import MockConfigEntry async def test_migration_creates_new_flow(hass, smartthings_mock, config_entry): """Test migration deletes app and creates new flow.""" + assert await async_setup_component(hass, "persistent_notification", {}) config_entry.version = 1 config_entry.add_to_hass(hass) @@ -50,6 +52,7 @@ async def test_unrecoverable_api_errors_create_new_flow( 403 (forbidden/not found): Occurs when the app or installed app could not be retrieved/found (likely deleted?) """ + assert await async_setup_component(hass, "persistent_notification", {}) config_entry.add_to_hass(hass) smartthings_mock.app.side_effect = ClientResponseError(None, None, status=401) diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index a5b1c04cb35..5f17606146b 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -18,7 +18,10 @@ class MockDevice(Device): def __init__(self, udn): """Initializer.""" - super().__init__(MagicMock()) + device = MagicMock() + device.manufacturer = "mock-manuf" + device.name = "mock-name" + super().__init__(device) self._udn = udn self.added_port_mappings = [] self.removed_port_mappings = [] From 172bbf806d46c4d73e3316247c7f4f8be23587c3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 12 Aug 2019 07:45:04 -0700 Subject: [PATCH 122/273] Add reproduce state support to input_boolean (#25858) * Add reproduce state support to input_boolean * Catch unknown state --- .../input_boolean/reproduce_state.py | 57 +++++++++++++++++++ .../input_boolean/test_reproduce_state.py | 41 +++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 homeassistant/components/input_boolean/reproduce_state.py create mode 100644 tests/components/input_boolean/test_reproduce_state.py diff --git a/homeassistant/components/input_boolean/reproduce_state.py b/homeassistant/components/input_boolean/reproduce_state.py new file mode 100644 index 00000000000..b8bc18edfac --- /dev/null +++ b/homeassistant/components/input_boolean/reproduce_state.py @@ -0,0 +1,57 @@ +"""Reproduce an input boolean state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, + STATE_OFF, + ATTR_ENTITY_ID, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def _async_reproduce_states( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce input boolean states.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in (STATE_ON, STATE_OFF): + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + if cur_state.state == state.state: + return + + service = SERVICE_TURN_ON if state.state == STATE_ON else SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, + service, + {ATTR_ENTITY_ID: state.entity_id}, + context=context, + blocking=True, + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce component states.""" + await asyncio.gather( + *(_async_reproduce_states(hass, state, context) for state in states) + ) diff --git a/tests/components/input_boolean/test_reproduce_state.py b/tests/components/input_boolean/test_reproduce_state.py new file mode 100644 index 00000000000..7ce4f4c1fd1 --- /dev/null +++ b/tests/components/input_boolean/test_reproduce_state.py @@ -0,0 +1,41 @@ +"""Test reproduce state for input boolean.""" +from homeassistant.core import State +from homeassistant.setup import async_setup_component + + +async def test_reproducing_states(hass): + """Test reproducing input_boolean states.""" + assert await async_setup_component( + hass, + "input_boolean", + { + "input_boolean": { + "initial_on": {"initial": True}, + "initial_off": {"initial": False}, + } + }, + ) + await hass.helpers.state.async_reproduce_state( + [ + State("input_boolean.initial_on", "off"), + State("input_boolean.initial_off", "on"), + # Should not raise + State("input_boolean.non_existing", "on"), + ], + blocking=True, + ) + assert hass.states.get("input_boolean.initial_off").state == "on" + assert hass.states.get("input_boolean.initial_on").state == "off" + + await hass.helpers.state.async_reproduce_state( + [ + # Test invalid state + State("input_boolean.initial_on", "invalid_state"), + # Set to state it already is. + State("input_boolean.initial_off", "on"), + ], + blocking=True, + ) + + assert hass.states.get("input_boolean.initial_on").state == "off" + assert hass.states.get("input_boolean.initial_off").state == "on" From 61b687edec0e098770987e2cec4e52fff3f1a7f8 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 12 Aug 2019 18:34:16 -0400 Subject: [PATCH 123/273] Bump ZHA dependencies. (#25898) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 081c77362b9..4c45b59aebb 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows-homeassistant==0.9.0", "zha-quirks==0.0.20", "zigpy-deconz==0.2.1", - "zigpy-homeassistant==0.7.0", + "zigpy-homeassistant==0.7.1", "zigpy-xbee-homeassistant==0.4.0", "zigpy-zigate==0.1.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index e41f16b1d0c..46e29bdbefe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1983,7 +1983,7 @@ ziggo-mediabox-xl==1.1.0 zigpy-deconz==0.2.1 # homeassistant.components.zha -zigpy-homeassistant==0.7.0 +zigpy-homeassistant==0.7.1 # homeassistant.components.zha zigpy-xbee-homeassistant==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f543f99f4b..902ebb16d5d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -403,4 +403,4 @@ wakeonlan==1.1.6 zeroconf==0.23.0 # homeassistant.components.zha -zigpy-homeassistant==0.7.0 +zigpy-homeassistant==0.7.1 From 34cde218763a34fc98aea8cf255726663ddd33ca Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 13 Aug 2019 09:09:55 +0100 Subject: [PATCH 124/273] Fix for HomeKit controller state not updating after put (#25903) --- .../homekit_controller/connection.py | 30 +++++++++++++++++-- tests/components/homekit_controller/common.py | 1 + 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index c4c00cb384b..5e703682f93 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -273,6 +273,12 @@ class HKDevice: # connection was dropped. return + self.process_new_events(new_values_dict) + + _LOGGER.debug("Finished HomeKit controller update") + + def process_new_events(self, new_values_dict): + """Process events from accessory into HA state.""" self.available = True for (aid, cid), value in new_values_dict.items(): @@ -281,8 +287,6 @@ class HKDevice: self.hass.helpers.dispatcher.async_dispatcher_send(self.signal_state_updated) - _LOGGER.debug("Finished HomeKit controller update") - async def get_characteristics(self, *args, **kwargs): """Read latest state from homekit accessory.""" async with self.pairing_lock: @@ -298,10 +302,30 @@ class HKDevice: chars.append((row["aid"], row["iid"], row["value"])) async with self.pairing_lock: - await self.hass.async_add_executor_job( + results = await self.hass.async_add_executor_job( self.pairing.put_characteristics, chars ) + # Feed characteristics back into HA and update the current state + # results will only contain failures, so anythin in characteristics + # but not in results was applied successfully - we can just have HA + # reflect the change immediately. + + new_entity_state = {} + for row in characteristics: + key = (row["aid"], row["iid"]) + + # If the key was returned by put_characteristics() then the + # change didnt work + if key in results: + continue + + # Otherwise it was accepted and we can apply the change to + # our state + new_entity_state[key] = {"value": row["value"]} + + self.process_new_events(new_entity_state) + @property def unique_id(self): """ diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 526884f86d4..2e1cfd7a77d 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -74,6 +74,7 @@ class FakePairing: if char.iid != cid: continue char.set_value(new_val) + return {} class FakeController: From e0ea5f2b04c97a3d8ef6451714ce55318b4e1897 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 13 Aug 2019 04:25:27 -0400 Subject: [PATCH 125/273] eq3bt: handle zero (closed valve) as a valid mode. fixes #25333 (#25905) --- homeassistant/components/eq3btsmart/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index 219578c34be..8499a0de5a0 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -91,7 +91,7 @@ class EQ3BTSmartThermostat(ClimateDevice): @property def available(self) -> bool: """Return if thermostat is available.""" - return self._thermostat.mode > 0 + return self._thermostat.mode >= 0 @property def name(self): From 1f2bab8e67a5462c42369e8814f0218203f9b806 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 13 Aug 2019 19:07:05 +0100 Subject: [PATCH 126/273] Fix Filter Sensor - check existing entity history (#25870) * make sure we have entity history * increase coverage * increase coverage * no need for list comprehension * increase coverage --- homeassistant/components/filter/sensor.py | 18 +++-- tests/components/filter/test_sensor.py | 89 +++++++++++++++++++++-- 2 files changed, 91 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index ccb0a5b12f5..2a8798d3729 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -242,7 +242,8 @@ class SensorFilter(Entity): entity_id=self._entity, ) ) - history_list.extend([state for state in filter_history[self._entity]]) + if self._entity in filter_history: + history_list.extend(filter_history[self._entity]) if largest_window_time > timedelta(seconds=0): start = dt_util.utcnow() - largest_window_time filter_history = await self.hass.async_add_job( @@ -253,13 +254,14 @@ class SensorFilter(Entity): entity_id=self._entity, ) ) - history_list.extend( - [ - state - for state in filter_history[self._entity] - if state not in history_list - ] - ) + if self._entity in filter_history: + history_list.extend( + [ + state + for state in filter_history[self._entity] + if state not in history_list + ] + ) # Sort the window states history_list = sorted(history_list, key=lambda s: s.last_updated) diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index bef0b66c433..6288e0699fd 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -59,6 +59,31 @@ class TestFilterSensor(unittest.TestCase): assert setup_component(self.hass, "sensor", config) def test_chain(self): + """Test if filter chaining works.""" + config = { + "sensor": { + "platform": "filter", + "name": "test", + "entity_id": "sensor.test_monitored", + "filters": [ + {"filter": "outlier", "window_size": 10, "radius": 4.0}, + {"filter": "lowpass", "time_constant": 10, "precision": 2}, + {"filter": "throttle", "window_size": 1}, + ], + } + } + + with assert_setup_component(1, "sensor"): + assert setup_component(self.hass, "sensor", config) + + for value in self.values: + self.hass.states.set(config["sensor"]["entity_id"], value.state) + self.hass.block_till_done() + + state = self.hass.states.get("sensor.test") + assert "18.05" == state.state + + def test_chain_history(self, missing=False): """Test if filter chaining works.""" self.init_recorder() config = { @@ -78,13 +103,16 @@ class TestFilterSensor(unittest.TestCase): t_1 = dt_util.utcnow() - timedelta(minutes=2) t_2 = dt_util.utcnow() - timedelta(minutes=3) - fake_states = { - "sensor.test_monitored": [ - ha.State("sensor.test_monitored", 18.0, last_changed=t_0), - ha.State("sensor.test_monitored", 19.0, last_changed=t_1), - ha.State("sensor.test_monitored", 18.2, last_changed=t_2), - ] - } + if missing: + fake_states = {} + else: + fake_states = { + "sensor.test_monitored": [ + ha.State("sensor.test_monitored", 18.0, last_changed=t_0), + ha.State("sensor.test_monitored", 19.0, last_changed=t_1), + ha.State("sensor.test_monitored", 18.2, last_changed=t_2), + ] + } with patch( "homeassistant.components.history." "state_changes_during_period", @@ -102,7 +130,52 @@ class TestFilterSensor(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get("sensor.test") - assert "17.05" == state.state + if missing: + assert "18.05" == state.state + else: + assert "17.05" == state.state + + def test_chain_history_missing(self): + """Test if filter chaining works when recorder is enabled but the source is not recorded.""" + return self.test_chain_history(missing=True) + + def test_history_time(self): + """Test loading from history based on a time window.""" + self.init_recorder() + config = { + "history": {}, + "sensor": { + "platform": "filter", + "name": "test", + "entity_id": "sensor.test_monitored", + "filters": [{"filter": "time_throttle", "window_size": "00:01"}], + }, + } + t_0 = dt_util.utcnow() - timedelta(minutes=1) + t_1 = dt_util.utcnow() - timedelta(minutes=2) + t_2 = dt_util.utcnow() - timedelta(minutes=3) + + fake_states = { + "sensor.test_monitored": [ + ha.State("sensor.test_monitored", 18.0, last_changed=t_0), + ha.State("sensor.test_monitored", 19.0, last_changed=t_1), + ha.State("sensor.test_monitored", 18.2, last_changed=t_2), + ] + } + with patch( + "homeassistant.components.history." "state_changes_during_period", + return_value=fake_states, + ): + with patch( + "homeassistant.components.history." "get_last_state_changes", + return_value=fake_states, + ): + with assert_setup_component(1, "sensor"): + assert setup_component(self.hass, "sensor", config) + + self.hass.block_till_done() + state = self.hass.states.get("sensor.test") + assert "18.0" == state.state def test_outlier(self): """Test if outlier filter works.""" From dc62671575bb26cfbd66e8d35f480370cf6a0ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Wed, 14 Aug 2019 03:09:07 +0800 Subject: [PATCH 127/273] Update syncthru library to fix issue (#25924) --- homeassistant/components/syncthru/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/syncthru/manifest.json b/homeassistant/components/syncthru/manifest.json index 8fc3b2476cb..a2a45826d9d 100644 --- a/homeassistant/components/syncthru/manifest.json +++ b/homeassistant/components/syncthru/manifest.json @@ -3,7 +3,7 @@ "name": "Syncthru", "documentation": "https://www.home-assistant.io/components/syncthru", "requirements": [ - "pysyncthru==0.4.2" + "pysyncthru==0.4.3" ], "dependencies": [], "codeowners": ["@nielstron"] diff --git a/requirements_all.txt b/requirements_all.txt index 46e29bdbefe..f6a97774c2b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1422,7 +1422,7 @@ pysuez==0.1.17 pysupla==0.0.3 # homeassistant.components.syncthru -pysyncthru==0.4.2 +pysyncthru==0.4.3 # homeassistant.components.tautulli pytautulli==0.5.0 From 45bbd080f1895e083046e6e9639d750ece693c40 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 14 Aug 2019 23:32:18 +1000 Subject: [PATCH 128/273] Bump version of library aio_geojson_geonetnz_quakes to v0.9 (#25946) --- homeassistant/components/geonetnz_quakes/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json index 44842133021..c84a4152582 100644 --- a/homeassistant/components/geonetnz_quakes/manifest.json +++ b/homeassistant/components/geonetnz_quakes/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/geonetnz_quakes", "requirements": [ - "aio_geojson_geonetnz_quakes==0.5" + "aio_geojson_geonetnz_quakes==0.9" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index f6a97774c2b..028c5515f52 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -115,7 +115,7 @@ adguardhome==0.2.1 afsapi==0.0.4 # homeassistant.components.geonetnz_quakes -aio_geojson_geonetnz_quakes==0.5 +aio_geojson_geonetnz_quakes==0.9 # homeassistant.components.ambient_station aioambient==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 902ebb16d5d..c447e5cb289 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -43,7 +43,7 @@ YesssSMS==0.2.3 adguardhome==0.2.1 # homeassistant.components.geonetnz_quakes -aio_geojson_geonetnz_quakes==0.5 +aio_geojson_geonetnz_quakes==0.9 # homeassistant.components.ambient_station aioambient==0.3.1 From 76b781829ece18b0e9c0f92d5fe386b8924cb919 Mon Sep 17 00:00:00 2001 From: Twan Coenraad Date: Wed, 14 Aug 2019 15:35:05 +0200 Subject: [PATCH 129/273] Add OTGW domestic hot water enable option as service (#25849) --- .../components/opentherm_gw/__init__.py | 27 +++++++++++++++++++ .../components/opentherm_gw/const.py | 2 ++ .../components/opentherm_gw/services.yaml | 17 ++++++++++++ 3 files changed, 46 insertions(+) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 7e91f78f74c..b20d97dadce 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -30,6 +30,7 @@ from .const import ( ATTR_GW_ID, ATTR_MODE, ATTR_LEVEL, + ATTR_DHW_OVRD, CONF_CLIMATE, CONF_FLOOR_TEMP, CONF_PRECISION, @@ -38,6 +39,7 @@ from .const import ( SERVICE_RESET_GATEWAY, SERVICE_SET_CLOCK, SERVICE_SET_CONTROL_SETPOINT, + SERVICE_SET_HOT_WATER_OVRD, SERVICE_SET_GPIO_MODE, SERVICE_SET_LED_MODE, SERVICE_SET_MAX_MOD, @@ -123,6 +125,16 @@ def register_services(hass): ), } ) + service_set_hot_water_ovrd_schema = vol.Schema( + { + vol.Required(ATTR_GW_ID): vol.All( + cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS]) + ), + vol.Required(ATTR_DHW_OVRD): vol.Any( + vol.Equal("A"), vol.All(vol.Coerce(int), vol.Range(min=0, max=1)) + ), + } + ) service_set_gpio_mode_schema = vol.Schema( vol.Any( vol.Schema( @@ -216,6 +228,21 @@ def register_services(hass): service_set_control_setpoint_schema, ) + async def set_dhw_ovrd(call): + """Set the domestic hot water override on the OpenTherm Gateway.""" + gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]] + gw_var = gw_vars.OTGW_DHW_OVRD + value = await gw_dev.gateway.set_hot_water_ovrd(call.data[ATTR_DHW_OVRD]) + gw_dev.status.update({gw_var: value}) + async_dispatcher_send(hass, gw_dev.update_signal, gw_dev.status) + + hass.services.async_register( + DOMAIN, + SERVICE_SET_HOT_WATER_OVRD, + set_dhw_ovrd, + service_set_hot_water_ovrd_schema, + ) + async def set_device_clock(call): """Set the clock on the OpenTherm Gateway.""" gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]] diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index 5b0411a2c72..77b0bf9b313 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -6,6 +6,7 @@ from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS ATTR_GW_ID = "gateway_id" ATTR_MODE = "mode" ATTR_LEVEL = "level" +ATTR_DHW_OVRD = "dhw_override" CONF_CLIMATE = "climate" CONF_FLOOR_TEMP = "floor_temperature" @@ -21,6 +22,7 @@ DEVICE_CLASS_PROBLEM = "problem" SERVICE_RESET_GATEWAY = "reset_gateway" SERVICE_SET_CLOCK = "set_clock" SERVICE_SET_CONTROL_SETPOINT = "set_control_setpoint" +SERVICE_SET_HOT_WATER_OVRD = "set_hot_water_ovrd" SERVICE_SET_GPIO_MODE = "set_gpio_mode" SERVICE_SET_LED_MODE = "set_led_mode" SERVICE_SET_MAX_MOD = "set_max_modulation" diff --git a/homeassistant/components/opentherm_gw/services.yaml b/homeassistant/components/opentherm_gw/services.yaml index d8fe2c7e406..4e9e727ef5a 100644 --- a/homeassistant/components/opentherm_gw/services.yaml +++ b/homeassistant/components/opentherm_gw/services.yaml @@ -35,6 +35,23 @@ set_control_setpoint: A value of 0 disables the central heating setpoint override. example: '37.5' +set_hot_water_ovrd: + description: > + Set the domestic hot water enable option on the gateway. + fields: + gateway_id: + description: The gateway_id of the OpenTherm Gateway. + example: 'opentherm_gateway' + dhw_override: + description: > + Control the domestic hot water enable option. If the boiler has + been configured to let the room unit control when to keep a + small amount of water preheated, this command can influence + that. + Value should be 0 or 1 to enable the override in off or on + state, or "A" to disable the override. + example: '1' + set_gpio_mode: description: Change the function of the GPIO pins of the gateway. fields: From 3b8668b2df4a5d5a58fbc993739b813950d04fed Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 14 Aug 2019 09:12:32 -0700 Subject: [PATCH 130/273] Bump androidtv to 0.0.23 (#25950) * Bump androidtv to 0.0.23 * Bump androidtv to 0.0.23 * Add missing space in logging statement --- homeassistant/components/androidtv/manifest.json | 2 +- homeassistant/components/androidtv/media_player.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index ec26cf44550..24eb61d52b0 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,7 +3,7 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/components/androidtv", "requirements": [ - "androidtv==0.0.22" + "androidtv==0.0.23" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 96d0a2ce00c..ef9293381fd 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -195,7 +195,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_name = config[CONF_NAME] if CONF_NAME in config else "Fire TV" add_entities([device]) - _LOGGER.debug("Setup %s at %s%s", device_name, host, adb_log) + _LOGGER.debug("Setup %s at %s %s", device_name, host, adb_log) hass.data[ANDROIDTV_DOMAIN][host] = device if hass.services.has_service(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND): diff --git a/requirements_all.txt b/requirements_all.txt index 028c5515f52..e62d28ff22c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -194,7 +194,7 @@ ambiclimate==0.2.0 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.22 +androidtv==0.0.23 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 From d8e2518e0d457539ecc90703c237abf189f61342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 14 Aug 2019 19:13:24 +0300 Subject: [PATCH 131/273] Use canonical pre-commit black URL (#25948) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5134f5f14aa..1b2cb9d69b1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: -- repo: https://github.com/python/black +- repo: https://github.com/psf/black rev: 19.3b0 hooks: - id: black From 002f74c76b46f86c5a27f8be9e33455936b485e2 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 14 Aug 2019 17:14:15 +0100 Subject: [PATCH 132/273] Update homekit_controller import style (#25940) --- .../components/homekit_controller/__init__.py | 12 +++------ .../homekit_controller/alarm_control_panel.py | 5 ++-- .../components/homekit_controller/climate.py | 5 ++-- .../homekit_controller/config_flow.py | 5 +--- .../homekit_controller/connection.py | 26 ++++++------------- .../components/homekit_controller/cover.py | 8 ++---- .../components/homekit_controller/light.py | 5 ++-- .../components/homekit_controller/lock.py | 5 ++-- .../components/homekit_controller/switch.py | 5 ++-- 9 files changed, 24 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 79636cea9f3..5ae82d0f124 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -1,6 +1,9 @@ """Support for Homekit device discovery.""" import logging +import homekit +from homekit.model.characteristics import CharacteristicsTypes + from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.exceptions import ConfigEntryNotReady @@ -63,9 +66,6 @@ class HomeKitEntity(Entity): def setup(self): """Configure an entity baed on its HomeKit characterstics metadata.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - accessories = self._accessory.accessories get_uuid = CharacteristicsTypes.get_uuid @@ -95,9 +95,6 @@ class HomeKitEntity(Entity): def _setup_characteristic(self, char): """Configure an entity based on a HomeKit characteristics metadata.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - # Build up a list of (aid, iid) tuples to poll on update() self.pollable_characteristics.append((self._aid, char["iid"])) @@ -211,9 +208,6 @@ async def async_setup_entry(hass, entry): async def async_setup(hass, config): """Set up for Homekit devices.""" - # pylint: disable=import-error - import homekit - map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass) await map_storage.async_initialize() diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index 38ed064f374..bb45a6c33d9 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -1,6 +1,8 @@ """Support for Homekit Alarm Control Panel.""" import logging +from homekit.model.characteristics import CharacteristicsTypes + from homeassistant.components.alarm_control_panel import AlarmControlPanel from homeassistant.const import ( ATTR_BATTERY_LEVEL, @@ -64,9 +66,6 @@ class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel): def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - return [ CharacteristicsTypes.SECURITY_SYSTEM_STATE_CURRENT, CharacteristicsTypes.SECURITY_SYSTEM_STATE_TARGET, diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index d295f607d71..194a2b5a42e 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -1,6 +1,8 @@ """Support for Homekit climate devices.""" import logging +from homekit.model.characteristics import CharacteristicsTypes + from homeassistant.components.climate import ( ClimateDevice, DEFAULT_MIN_HUMIDITY, @@ -84,9 +86,6 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - return [ CharacteristicsTypes.HEATING_COOLING_CURRENT, CharacteristicsTypes.HEATING_COOLING_TARGET, diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index e5337295a70..008e0f8566d 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -3,6 +3,7 @@ import os import json import logging +import homekit import voluptuous as vol from homeassistant import config_entries @@ -62,8 +63,6 @@ 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 = {} @@ -224,8 +223,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): async def async_step_pair(self, pair_info=None): """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. diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 5e703682f93..8068f26aaa3 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -3,6 +3,14 @@ import asyncio import datetime import logging +from homekit.exceptions import ( + AccessoryDisconnectedError, + AccessoryNotFoundError, + EncryptionError, +) +from homekit.model.services import ServicesTypes +from homekit.model.characteristics import CharacteristicsTypes + from homeassistant.helpers.event import async_track_time_interval from .const import DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, ENTITY_MAP @@ -16,10 +24,6 @@ _LOGGER = logging.getLogger(__name__) def get_accessory_information(accessory): """Obtain the accessory information service of a HomeKit device.""" - # pylint: disable=import-error - from homekit.model.services import ServicesTypes - from homekit.model.characteristics import CharacteristicsTypes - result = {} for service in accessory["services"]: stype = service["type"].upper() @@ -163,9 +167,6 @@ class HKDevice: 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: async with self.pairing_lock: self.accessories = await self.hass.async_add_executor_job( @@ -205,8 +206,6 @@ class HKDevice: 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"]: @@ -225,8 +224,6 @@ class HKDevice: def async_load_platforms(self): """Load any platforms needed by this HomeKit device.""" - from homekit.model.services import ServicesTypes - for accessory in self.accessories: for service in accessory["services"]: stype = ServicesTypes.get_short(service["type"].upper()) @@ -246,13 +243,6 @@ class HKDevice: async def async_update(self, now=None): """Poll state of all entities attached to this bridge/accessory.""" - # pylint: disable=import-error - from homekit.exceptions import ( - AccessoryDisconnectedError, - AccessoryNotFoundError, - EncryptionError, - ) - if not self.pollable_characteristics: _LOGGER.debug("HomeKit connection not polling any characteristics.") return diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index c15e0c092ac..7f70b0cfac0 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -1,6 +1,8 @@ """Support for Homekit covers.""" import logging +from homekit.model.characteristics import CharacteristicsTypes + from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, @@ -76,9 +78,6 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice): def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - return [ CharacteristicsTypes.DOOR_STATE_CURRENT, CharacteristicsTypes.DOOR_STATE_TARGET, @@ -154,9 +153,6 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice): def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - return [ CharacteristicsTypes.POSITION_STATE, CharacteristicsTypes.POSITION_CURRENT, diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index 534a8c7cd18..fe2a0e9bc97 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -1,6 +1,8 @@ """Support for Homekit lights.""" import logging +from homekit.model.characteristics import CharacteristicsTypes + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -50,9 +52,6 @@ class HomeKitLight(HomeKitEntity, Light): def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - return [ CharacteristicsTypes.ON, CharacteristicsTypes.BRIGHTNESS, diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py index 4ca118acee6..53f7bb5dfd5 100644 --- a/homeassistant/components/homekit_controller/lock.py +++ b/homeassistant/components/homekit_controller/lock.py @@ -1,6 +1,8 @@ """Support for HomeKit Controller locks.""" import logging +from homekit.model.characteristics import CharacteristicsTypes + from homeassistant.components.lock import LockDevice from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED @@ -46,9 +48,6 @@ class HomeKitLock(HomeKitEntity, LockDevice): def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - return [ CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE, CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE, diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index 7fdc6a7082f..7eedda1b191 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -1,6 +1,8 @@ """Support for Homekit switches.""" import logging +from homekit.model.characteristics import CharacteristicsTypes + from homeassistant.components.switch import SwitchDevice from . import KNOWN_DEVICES, HomeKitEntity @@ -41,9 +43,6 @@ class HomeKitSwitch(HomeKitEntity, SwitchDevice): def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - return [CharacteristicsTypes.ON, CharacteristicsTypes.OUTLET_IN_USE] def _update_on(self, value): From bd8461b8992c13b28657ab78c73357c1f8a6f514 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 14 Aug 2019 11:15:37 -0500 Subject: [PATCH 133/273] Handle more Life360 errors in config flow & bump package to 4.1.1 (#25930) --- .../components/life360/.translations/en.json | 5 +++-- homeassistant/components/life360/config_flow.py | 12 +++++++++++- homeassistant/components/life360/manifest.json | 2 +- homeassistant/components/life360/strings.json | 3 ++- requirements_all.txt | 2 +- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/life360/.translations/en.json b/homeassistant/components/life360/.translations/en.json index 2c187ba0470..d601c9cfe8e 100644 --- a/homeassistant/components/life360/.translations/en.json +++ b/homeassistant/components/life360/.translations/en.json @@ -10,7 +10,8 @@ "error": { "invalid_credentials": "Invalid credentials", "invalid_username": "Invalid username", - "user_already_configured": "Account has already been configured" + "user_already_configured": "Account has already been configured", + "unexpected": "Unexpected error communicating with Life360 server" }, "step": { "user": { @@ -24,4 +25,4 @@ }, "title": "Life360" } -} \ No newline at end of file +} diff --git a/homeassistant/components/life360/config_flow.py b/homeassistant/components/life360/config_flow.py index 9bd33981f37..be84d276422 100644 --- a/homeassistant/components/life360/config_flow.py +++ b/homeassistant/components/life360/config_flow.py @@ -2,7 +2,7 @@ from collections import OrderedDict import logging -from life360 import LoginError +from life360 import Life360Error, LoginError import voluptuous as vol from homeassistant import config_entries @@ -54,6 +54,11 @@ class Life360ConfigFlow(config_entries.ConfigFlow): errors[CONF_USERNAME] = "invalid_username" except LoginError: errors["base"] = "invalid_credentials" + except Life360Error as error: + _LOGGER.error( + "Unexpected error communicating with Life360 server: %s", error + ) + errors["base"] = "unexpected" else: if self._username in self.configured_usernames: errors["base"] = "user_already_configured" @@ -88,6 +93,11 @@ class Life360ConfigFlow(config_entries.ConfigFlow): except LoginError: _LOGGER.error("Invalid credentials for %s", username) return self.async_abort(reason="invalid_credentials") + except Life360Error as error: + _LOGGER.error( + "Unexpected error communicating with Life360 server: %s", error + ) + return self.async_abort(reason="unexpected") return self.async_create_entry( title="{} (from configuration)".format(username), data={ diff --git a/homeassistant/components/life360/manifest.json b/homeassistant/components/life360/manifest.json index 079344af6a6..9eae371070a 100644 --- a/homeassistant/components/life360/manifest.json +++ b/homeassistant/components/life360/manifest.json @@ -8,6 +8,6 @@ "@pnbruckner" ], "requirements": [ - "life360==4.0.1" + "life360==4.1.1" ] } diff --git a/homeassistant/components/life360/strings.json b/homeassistant/components/life360/strings.json index cff3f39e5d5..419f3650333 100644 --- a/homeassistant/components/life360/strings.json +++ b/homeassistant/components/life360/strings.json @@ -14,7 +14,8 @@ "error": { "invalid_username": "Invalid username", "invalid_credentials": "Invalid credentials", - "user_already_configured": "Account has already been configured" + "user_already_configured": "Account has already been configured", + "unexpected": "Unexpected error communicating with Life360 server" }, "create_entry": { "default": "To set advanced options, see [Life360 documentation]({docs_url})." diff --git a/requirements_all.txt b/requirements_all.txt index e62d28ff22c..36855c23bf1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -723,7 +723,7 @@ librouteros==2.3.0 libsoundtouch==0.7.2 # homeassistant.components.life360 -life360==4.0.1 +life360==4.1.1 # homeassistant.components.lifx_legacy liffylights==0.9.4 From cbcf49a3bad3a7627954c21ff44ff808639f4793 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 14 Aug 2019 18:17:58 +0200 Subject: [PATCH 134/273] Improve Sonos error handling on slow networks (#25902) --- homeassistant/components/sonos/manifest.json | 2 +- .../components/sonos/media_player.py | 20 +++++++++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 4e65edc1f5b..8c231ec63e0 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/sonos", "requirements": [ - "pysonos==0.0.22" + "pysonos==0.0.23" ], "dependencies": [], "ssdp": { diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 38ae99f2d3f..a4d461f289f 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -102,6 +102,7 @@ class SonosData: def __init__(self, hass): """Initialize the data.""" self.entities = [] + self.discovered = [] self.topology_condition = asyncio.Condition() @@ -132,14 +133,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Handle a (re)discovered player.""" try: _LOGGER.debug("Reached _discovered_player, soco=%s", soco) - entity = _get_entity_from_soco_uid(hass, soco.uid) - if not entity: + if soco not in hass.data[DATA_SONOS].discovered: _LOGGER.debug("Adding new entity") + hass.data[DATA_SONOS].discovered.append(soco) hass.add_job(async_add_entities, [SonosEntity(soco)]) else: - _LOGGER.debug("Seen %s", entity) - hass.add_job(entity.async_seen()) + entity = _get_entity_from_soco_uid(hass, soco.uid) + if entity: + _LOGGER.debug("Seen %s", entity) + hass.add_job(entity.async_seen()) except SoCoException as ex: _LOGGER.debug("SoCoException, ex=%s", ex) @@ -222,7 +225,10 @@ class _ProcessSonosEventQueue: def put(self, item, block=True, timeout=None): """Process event.""" - self._handler(item) + try: + self._handler(item) + except SoCoException as ex: + _LOGGER.warning("Error calling %s: %s", self._handler, ex) def _get_entity_from_soco_uid(hass, uid): @@ -318,7 +324,7 @@ class SonosEntity(MediaPlayerDevice): self._night_sound = None self._speech_enhance = None self._source_name = None - self._favorites = None + self._favorites = [] self._soco_snapshot = None self._snapshot_group = None @@ -748,6 +754,8 @@ class SonosEntity(MediaPlayerDevice): @property def volume_level(self): """Volume level of the media player (0..1).""" + if self._player_volume is None: + return None return self._player_volume / 100 @property diff --git a/requirements_all.txt b/requirements_all.txt index 36855c23bf1..044552371ea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1404,7 +1404,7 @@ pysmarty==0.8 pysnmp==4.4.9 # homeassistant.components.sonos -pysonos==0.0.22 +pysonos==0.0.23 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c447e5cb289..dac3562bf79 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -316,7 +316,7 @@ pysmartapp==0.3.2 pysmartthings==0.6.9 # homeassistant.components.sonos -pysonos==0.0.22 +pysonos==0.0.23 # homeassistant.components.spc pyspcwebgw==0.4.0 From a71e8851ae22eb22eefa1d304e3323f75efce990 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 14 Aug 2019 18:03:11 -0600 Subject: [PATCH 135/273] Bump simplisafe-python to 4.3.0 (#25955) --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 130a9d23a3a..8a03ac47402 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/simplisafe", "requirements": [ - "simplisafe-python==4.2.0" + "simplisafe-python==4.3.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 044552371ea..91017cdd3a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1699,7 +1699,7 @@ shodan==1.13.0 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==4.2.0 +simplisafe-python==4.3.0 # homeassistant.components.sisyphus sisyphus-control==2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dac3562bf79..be63d88805f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -358,7 +358,7 @@ ring_doorbell==0.2.3 rxv==0.6.0 # homeassistant.components.simplisafe -simplisafe-python==4.2.0 +simplisafe-python==4.3.0 # homeassistant.components.sleepiq sleepyq==0.7 From b8bd97b3fab0942c4bf045cbad7115c49389f0f0 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 14 Aug 2019 19:56:53 -0600 Subject: [PATCH 136/273] Bump aioambient to 0.3.2 (#25956) --- homeassistant/components/ambient_station/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index 510edd540ec..056930edfc7 100644 --- a/homeassistant/components/ambient_station/manifest.json +++ b/homeassistant/components/ambient_station/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/ambient_station", "requirements": [ - "aioambient==0.3.1" + "aioambient==0.3.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 91017cdd3a5..0008a61c872 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -118,7 +118,7 @@ afsapi==0.0.4 aio_geojson_geonetnz_quakes==0.9 # homeassistant.components.ambient_station -aioambient==0.3.1 +aioambient==0.3.2 # homeassistant.components.asuswrt aioasuswrt==1.1.21 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index be63d88805f..6e07a9fb084 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -46,7 +46,7 @@ adguardhome==0.2.1 aio_geojson_geonetnz_quakes==0.9 # homeassistant.components.ambient_station -aioambient==0.3.1 +aioambient==0.3.2 # homeassistant.components.automatic aioautomatic==0.6.5 From 944ac4f3c2c4db1e0af62735b20b1520132c0faa Mon Sep 17 00:00:00 2001 From: Andrew Chatham Date: Thu, 15 Aug 2019 04:40:39 -0700 Subject: [PATCH 137/273] Expose Lutron RA2 occupancy sensors (#25854) * Expose Lutron RA2 motion sensors * Remove unused _LOGGER * Remove unused logging import --- homeassistant/components/lutron/__init__.py | 16 +++++- .../components/lutron/binary_sensor.py | 56 +++++++++++++++++++ homeassistant/components/lutron/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/lutron/binary_sensor.py diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index 1ef9a9374f7..9f4d81df044 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -40,7 +40,13 @@ def setup(hass, base_config): hass.data[LUTRON_BUTTONS] = [] hass.data[LUTRON_CONTROLLER] = None - hass.data[LUTRON_DEVICES] = {"light": [], "cover": [], "switch": [], "scene": []} + hass.data[LUTRON_DEVICES] = { + "light": [], + "cover": [], + "switch": [], + "scene": [], + "binary_sensor": [], + } config = base_config.get(DOMAIN) hass.data[LUTRON_CONTROLLER] = Lutron( @@ -76,9 +82,13 @@ def setup(hass, base_config): ) hass.data[LUTRON_BUTTONS].append(LutronButton(hass, keypad, button)) + if area.occupancy_group is not None: + hass.data[LUTRON_DEVICES]["binary_sensor"].append( + (area.name, area.occupancy_group) + ) - for component in ("light", "cover", "switch", "scene"): - discovery.load_platform(hass, component, DOMAIN, None, base_config) + for component in ("light", "cover", "switch", "scene", "binary_sensor"): + discovery.load_platform(hass, component, DOMAIN, {}, base_config) return True diff --git a/homeassistant/components/lutron/binary_sensor.py b/homeassistant/components/lutron/binary_sensor.py new file mode 100644 index 00000000000..a86d56c325f --- /dev/null +++ b/homeassistant/components/lutron/binary_sensor.py @@ -0,0 +1,56 @@ +"""Support for Lutron Powr Savr occupancy sensors.""" +from pylutron import OccupancyGroup + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, + DEVICE_CLASS_OCCUPANCY, +) + +from . import LUTRON_CONTROLLER, LUTRON_DEVICES, LutronDevice + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Lutron occupancy sensors.""" + if discovery_info is None: + return + devs = [] + for (area_name, device) in hass.data[LUTRON_DEVICES]["binary_sensor"]: + dev = LutronOccupancySensor(area_name, device, hass.data[LUTRON_CONTROLLER]) + devs.append(dev) + + add_entities(devs) + + +class LutronOccupancySensor(LutronDevice, BinarySensorDevice): + """Representation of a Lutron Occupancy Group. + + The Lutron integration API reports "occupancy groups" rather than + individual sensors. If two sensors are in the same room, they're + reported as a single occupancy group. + """ + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + # Error cases will end up treated as unoccupied. + return self._lutron_device.state == OccupancyGroup.State.OCCUPIED + + @property + def device_class(self): + """Return that this is an occupancy sensor.""" + return DEVICE_CLASS_OCCUPANCY + + @property + def name(self): + """Return the name of the device.""" + # The default LutronDevice naming would create 'Kitchen Occ Kitchen', + # but since there can only be one OccupancyGroup per area we go + # with something shorter. + return f"{self._area_name} Occupancy" + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attr = {} + attr["lutron_integration_id"] = self._lutron_device.id + return attr diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index ab7ca3d919f..bece55ae09d 100644 --- a/homeassistant/components/lutron/manifest.json +++ b/homeassistant/components/lutron/manifest.json @@ -3,7 +3,7 @@ "name": "Lutron", "documentation": "https://www.home-assistant.io/components/lutron", "requirements": [ - "pylutron==0.2.1" + "pylutron==0.2.2" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 0008a61c872..f05be3a96d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1258,7 +1258,7 @@ pyloopenergy==0.1.3 pylutron-caseta==0.5.0 # homeassistant.components.lutron -pylutron==0.2.1 +pylutron==0.2.2 # homeassistant.components.mailgun pymailgunner==1.4 From 53ad33e84a0609cf2db7b2debaaedd598ad3d5ab Mon Sep 17 00:00:00 2001 From: zewelor Date: Thu, 15 Aug 2019 13:41:54 +0200 Subject: [PATCH 138/273] Add volumio shuffle support (#25871) * Add volumio shuffle support * Black reformat * Update homeassistant/components/volumio/media_player.py Co-Authored-By: Martin Hjelmare --- homeassistant/components/volumio/media_player.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index 869d3cb3ed5..96e1d883646 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -29,6 +29,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, + SUPPORT_SHUFFLE_SET, ) from homeassistant.const import ( CONF_HOST, @@ -65,6 +66,7 @@ SUPPORT_VOLUMIO = ( | SUPPORT_PLAY | SUPPORT_VOLUME_STEP | SUPPORT_SELECT_SOURCE + | SUPPORT_SHUFFLE_SET | SUPPORT_CLEAR_PLAYLIST ) @@ -231,6 +233,11 @@ class Volumio(MediaPlayerDevice): """Return the name of the device.""" return self._name + @property + def shuffle(self): + """Boolean if shuffle is enabled.""" + return self._state.get("random", False) + @property def source_list(self): """Return the list of available input sources.""" @@ -296,6 +303,12 @@ class Volumio(MediaPlayerDevice): "commands", params={"cmd": "volume", "volume": self._lastvol} ) + def async_set_shuffle(self, shuffle): + """Enable/disable shuffle mode.""" + return self.send_volumio_msg( + "commands", params={"cmd": "random", "value": str(shuffle)} + ) + def async_select_source(self, source): """Choose a different available playlist and play it.""" self._currentplaylist = source From 3525728abca50515ac9f1fad9a1ae8aec5ab7e60 Mon Sep 17 00:00:00 2001 From: Alex Fung Date: Fri, 16 Aug 2019 00:52:38 +1200 Subject: [PATCH 139/273] Fix Tile Errors (#25866) * Fix Tile Errors * Black formatting --- homeassistant/components/tile/device_tracker.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 96ad1211812..e8ed5b06d27 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -126,14 +126,17 @@ class TileScanner: for tile in tiles: await self._async_see( dev_id="tile_{0}".format(slugify(tile["tile_uuid"])), - gps=(tile["tileState"]["latitude"], tile["tileState"]["longitude"]), + gps=( + tile["last_tile_state"]["latitude"], + tile["last_tile_state"]["longitude"], + ), attributes={ - ATTR_ALTITUDE: tile["tileState"]["altitude"], - ATTR_CONNECTION_STATE: tile["tileState"]["connection_state"], + ATTR_ALTITUDE: tile["last_tile_state"]["altitude"], + ATTR_CONNECTION_STATE: tile["last_tile_state"]["connection_state"], ATTR_IS_DEAD: tile["is_dead"], - ATTR_IS_LOST: tile["tileState"]["is_lost"], - ATTR_RING_STATE: tile["tileState"]["ring_state"], - ATTR_VOIP_STATE: tile["tileState"]["voip_state"], + ATTR_IS_LOST: tile["last_tile_state"]["is_lost"], + ATTR_RING_STATE: tile["last_tile_state"]["ring_state"], + ATTR_VOIP_STATE: tile["last_tile_state"]["voip_state"], ATTR_TILE_ID: tile["tile_uuid"], ATTR_TILE_NAME: tile["name"], }, From aa508b5106d0fd650dba8de1c7373f2ee6abe786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 15 Aug 2019 18:53:25 +0300 Subject: [PATCH 140/273] Complete some incomplete type hints in helpers (#25953) --- homeassistant/helpers/check_config.py | 3 +-- homeassistant/helpers/config_entry_flow.py | 7 +++++-- homeassistant/helpers/device_registry.py | 6 ++++-- homeassistant/helpers/entity.py | 11 +++++------ homeassistant/helpers/entity_registry.py | 6 ++++-- homeassistant/helpers/service.py | 5 +++-- homeassistant/helpers/storage.py | 12 ++++++------ 7 files changed, 28 insertions(+), 22 deletions(-) diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 331599e9b0f..bc39d5d5720 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -27,8 +27,7 @@ import homeassistant.util.yaml.loader as yaml_loader from homeassistant.exceptions import HomeAssistantError -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs -# mypy: no-warn-return-any +# mypy: allow-untyped-calls, allow-untyped-defs, no-warn-return-any CheckConfigError = namedtuple("CheckConfigError", "message domain config") diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 28b2fb495d0..f341cc2ce02 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -2,9 +2,10 @@ from functools import partial from homeassistant import config_entries +from .typing import HomeAssistantType -# mypy: allow-incomplete-defs, allow-untyped-defs +# mypy: allow-untyped-defs def register_discovery_flow(domain, title, discovery_function, connection_class): @@ -130,7 +131,9 @@ class WebhookFlowHandler(config_entries.ConfigFlow): ) -async def webhook_async_remove_entry(hass, entry) -> None: +async def webhook_async_remove_entry( + hass: HomeAssistantType, entry: config_entries.ConfigEntry +) -> None: """Remove a webhook config entry.""" if not entry.data.get("cloudhook") or "cloud" not in hass.config.components: return diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index a4d83c4eba9..19b4a1333b6 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -13,7 +13,7 @@ from homeassistant.loader import bind_hass from .typing import HomeAssistantType -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any _LOGGER = logging.getLogger(__name__) @@ -84,7 +84,9 @@ class DeviceRegistry: return self.devices.get(device_id) @callback - def async_get_device(self, identifiers: set, connections: set): + def async_get_device( + self, identifiers: set, connections: set + ) -> Optional[DeviceEntry]: """Check if device is registered.""" for device in self.devices.values(): if any(iden in device.identifiers for iden in identifiers) or any( diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index fa4352822ed..6508de08143 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging import functools as ft from timeit import default_timer as timer -from typing import Optional, List, Iterable +from typing import Any, Optional, List, Iterable from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -34,8 +34,7 @@ from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util import dt as dt_util -# mypy: allow-incomplete-defs, allow-untyped-defs, no-check-untyped-defs -# mypy: no-warn-return-any +# mypy: allow-untyped-defs, no-check-untyped-defs, no-warn-return-any _LOGGER = logging.getLogger(__name__) SLOW_UPDATE_WARNING = 10 @@ -532,7 +531,7 @@ class ToggleEntity(Entity): """Return True if entity is on.""" raise NotImplementedError() - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" raise NotImplementedError() @@ -543,7 +542,7 @@ class ToggleEntity(Entity): """ return self.hass.async_add_job(ft.partial(self.turn_on, **kwargs)) - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" raise NotImplementedError() @@ -554,7 +553,7 @@ class ToggleEntity(Entity): """ return self.hass.async_add_job(ft.partial(self.turn_off, **kwargs)) - def toggle(self, **kwargs) -> None: + def toggle(self, **kwargs: Any) -> None: """Toggle the entity.""" if self.is_on: self.turn_off(**kwargs) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index bb546ba7b83..9a7be9ecc36 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -24,7 +24,7 @@ from homeassistant.util.yaml import load_yaml from .typing import HomeAssistantType -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any PATH_REGISTRY = "entity_registry.yaml" @@ -91,7 +91,9 @@ class EntityRegistry: return self.entities.get(entity_id) @callback - def async_get_entity_id(self, domain: str, platform: str, unique_id: str): + def async_get_entity_id( + self, domain: str, platform: str, unique_id: str + ) -> Optional[str]: """Check if an entity_id is currently registered.""" for entity in self.entities.values(): if ( diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index d702fce8d8b..07e070df8c5 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -18,12 +18,13 @@ from homeassistant.exceptions import ( from homeassistant.helpers import template, typing from homeassistant.loader import async_get_integration, bind_hass from homeassistant.util.yaml import load_yaml +from homeassistant.util.yaml.loader import JSON_TYPE import homeassistant.helpers.config_validation as cv from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.helpers.typing import HomeAssistantType -# mypy: allow-incomplete-defs, allow-untyped-defs, no-check-untyped-defs +# mypy: allow-untyped-defs, no-check-untyped-defs CONF_SERVICE = "service" CONF_SERVICE_TEMPLATE = "service_template" @@ -161,7 +162,7 @@ async def async_extract_entity_ids(hass, service_call, expand_group=True): return extracted -async def _load_services_file(hass: HomeAssistantType, domain: str): +async def _load_services_file(hass: HomeAssistantType, domain: str) -> JSON_TYPE: """Load services file for an integration.""" integration = await async_get_integration(hass, domain) try: diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 5bb912adafe..368753cd626 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -6,14 +6,13 @@ import os from typing import Dict, List, Optional, Callable, Union, Any, Type from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.loader import bind_hass from homeassistant.util import json as json_util from homeassistant.helpers.event import async_call_later -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs -# mypy: no-warn-return-any +# mypy: allow-untyped-calls, allow-untyped-defs, no-warn-return-any STORAGE_DIR = ".storage" _LOGGER = logging.getLogger(__name__) @@ -59,7 +58,7 @@ class Store: def __init__( self, - hass, + hass: HomeAssistant, version: int, key: str, private: bool = False, @@ -94,6 +93,7 @@ class Store: """ if self._load_task is None: self._load_task = self.hass.async_add_job(self._async_load()) + assert self._load_task is not None return await self._load_task @@ -138,7 +138,7 @@ class Store: @callback def async_delay_save( self, data_func: Callable[[], Dict], delay: Optional[int] = None - ): + ) -> None: """Save data with an optional delay.""" self._data = {"version": self.version, "key": self.key, "data_func": data_func} @@ -201,7 +201,7 @@ class Store: except (json_util.SerializationError, json_util.WriteError) as err: _LOGGER.error("Error writing config for %s: %s", self.key, err) - def _write_data(self, path: str, data: Dict): + def _write_data(self, path: str, data: Dict) -> None: """Write the data.""" if not os.path.isdir(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) From df3fbc5715ce0137d7909f0266e81e7be887394f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 15 Aug 2019 14:04:03 -0700 Subject: [PATCH 141/273] Update translations --- homeassistant/components/life360/.translations/en.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/life360/.translations/en.json b/homeassistant/components/life360/.translations/en.json index d601c9cfe8e..2c187ba0470 100644 --- a/homeassistant/components/life360/.translations/en.json +++ b/homeassistant/components/life360/.translations/en.json @@ -10,8 +10,7 @@ "error": { "invalid_credentials": "Invalid credentials", "invalid_username": "Invalid username", - "user_already_configured": "Account has already been configured", - "unexpected": "Unexpected error communicating with Life360 server" + "user_already_configured": "Account has already been configured" }, "step": { "user": { @@ -25,4 +24,4 @@ }, "title": "Life360" } -} +} \ No newline at end of file From 6d1d95394c9b88b865183c8daa8c938cab65de46 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 15 Aug 2019 14:04:17 -0700 Subject: [PATCH 142/273] Updated frontend to 20190815.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 9a39e61e727..2ccda8a1f4a 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==20190811.0" + "home-assistant-frontend==20190815.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7a43109bf1f..daac63bee9b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.16 -home-assistant-frontend==20190811.0 +home-assistant-frontend==20190815.0 importlib-metadata==0.18 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index f05be3a96d8..b970f95e763 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -630,7 +630,7 @@ hole==0.3.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190811.0 +home-assistant-frontend==20190815.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e07a9fb084..85e44ccca44 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -177,7 +177,7 @@ hdate==0.9.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190811.0 +home-assistant-frontend==20190815.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 9b3aa9bbd12e4f763df11015ab14fce3e5835626 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 15 Aug 2019 14:11:39 -0700 Subject: [PATCH 143/273] Remove uvloop from default install and warn about stream+shell_command (#25929) * Add warning about uvloop and shell_command * Remove uvloop from docker files" * Add ffmpeg --- Dockerfile | 2 +- homeassistant/components/stream/__init__.py | 22 ++++++++++++++++++++- virtualization/Docker/Dockerfile.dev | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index a9e73699558..ebd802374eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,7 @@ RUN virtualization/Docker/setup_docker_prereqs # Install hass component dependencies COPY requirements_all.txt requirements_all.txt RUN pip3 install --no-cache-dir -r requirements_all.txt && \ - pip3 install --no-cache-dir mysqlclient psycopg2 uvloop==0.12.2 cchardet cython tensorflow + pip3 install --no-cache-dir mysqlclient psycopg2 cchardet cython tensorflow # Copy source COPY . . diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 4fd3e71a48b..50cc1d8169d 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -4,6 +4,11 @@ import threading import voluptuous as vol +try: + import uvloop +except ImportError: + uvloop = None + from homeassistant.auth.util import generate_secret import homeassistant.helpers.config_validation as cv from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_FILENAME @@ -38,7 +43,7 @@ SERVICE_RECORD_SCHEMA = STREAM_SERVICE_SCHEMA.extend( vol.Optional(CONF_LOOKBACK, default=0): int, } ) - +DATA_UVLOOP_WARN = "stream_uvloop_warn" # Set log level to error for libav logging.getLogger("libav").setLevel(logging.ERROR) @@ -49,6 +54,21 @@ def request_stream(hass, stream_source, *, fmt="hls", keepalive=False, options=N if DOMAIN not in hass.config.components: raise HomeAssistantError("Stream integration is not set up.") + if DATA_UVLOOP_WARN not in hass.data: + hass.data[DATA_UVLOOP_WARN] = True + # Warn about https://github.com/home-assistant/home-assistant/issues/22999 + if ( + uvloop is not None + and isinstance(hass.loop, uvloop.Loop) + and ( + "shell_command" in hass.config.components + or "ffmpeg" in hass.config.components + ) + ): + _LOGGER.warning( + "You are using UVLoop with stream and shell_command. This is known to cause issues. Please uninstall uvloop." + ) + if options is None: options = {} diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev index 260a29cb3d0..2e98c53cdf1 100644 --- a/virtualization/Docker/Dockerfile.dev +++ b/virtualization/Docker/Dockerfile.dev @@ -28,7 +28,7 @@ RUN virtualization/Docker/setup_docker_prereqs COPY requirements_all.txt requirements_all.txt RUN pip3 install --no-cache-dir -r requirements_all.txt && \ - pip3 install --no-cache-dir mysqlclient psycopg2 uvloop==0.12.2 cchardet cython + pip3 install --no-cache-dir mysqlclient psycopg2 cchardet cython # BEGIN: Development additions From 8a1ab8c0b504ec6cd7c941065cd79b4fbd1a1004 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 15 Aug 2019 14:11:55 -0700 Subject: [PATCH 144/273] Tweaks to options flow (#25969) --- .../components/config/config_entries.py | 4 ++-- homeassistant/config_entries.py | 18 ++++++++++++++++- .../components/config/test_config_entries.py | 20 ++++++------------- tests/test_config_entries.py | 7 +++---- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 140b5a2b270..68967439b2a 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -187,8 +187,8 @@ class ConfigManagerAvailableFlowView(HomeAssistantView): class OptionManagerFlowIndexView(FlowManagerIndexView): """View to create option flows.""" - url = "/api/config/config_entries/entry/option/flow" - name = "api:config:config_entries:entry:resource:option:flow" + url = "/api/config/config_entries/options/flow" + name = "api:config:config_entries:option:flow" # pylint: disable=arguments-differ async def post(self, request): diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index fb064f63075..46c324110ec 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -658,6 +658,12 @@ class ConfigFlow(data_entry_flow.FlowHandler): CONNECTION_CLASS = CONN_CLASS_UNKNOWN + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + raise data_entry_flow.UnknownHandler + @callback def _async_current_entries(self): """Return current entries.""" @@ -691,7 +697,11 @@ class OptionsFlowManager: entry = self.hass.config_entries.async_get_entry(entry_id) if entry is None: return - flow = HANDLERS[entry.domain].async_get_options_flow(entry.data, entry.options) + + if entry.domain not in HANDLERS: + raise data_entry_flow.UnknownHandler + + flow = HANDLERS[entry.domain].async_get_options_flow(entry) return flow async def _async_finish_flow(self, flow, result): @@ -706,3 +716,9 @@ class OptionsFlowManager: result["result"] = True return result + + +class OptionsFlow(data_entry_flow.FlowHandler): + """Base class for config option flows.""" + + pass diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 563fc639b76..efe476b7055 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -488,12 +488,8 @@ async def test_options_flow(hass, client): class TestFlow(core_ce.ConfigFlow): @staticmethod @callback - def async_get_options_flow(config, options): + def async_get_options_flow(config_entry): class OptionsFlowHandler(data_entry_flow.FlowHandler): - def __init__(self, config, options): - self.config = config - self.options = options - async def async_step_init(self, user_input=None): schema = OrderedDict() schema[vol.Required("enabled")] = bool @@ -503,7 +499,7 @@ async def test_options_flow(hass, client): description_placeholders={"enabled": "Set to true to be true"}, ) - return OptionsFlowHandler(config, options) + return OptionsFlowHandler() MockConfigEntry( domain="test", @@ -514,7 +510,7 @@ async def test_options_flow(hass, client): entry = hass.config_entries._entries[0] with patch.dict(HANDLERS, {"test": TestFlow}): - url = "/api/config/config_entries/entry/option/flow" + url = "/api/config/config_entries/options/flow" resp = await client.post(url, json={"handler": entry.entry_id}) assert resp.status == 200 @@ -538,12 +534,8 @@ async def test_two_step_options_flow(hass, client): class TestFlow(core_ce.ConfigFlow): @staticmethod @callback - def async_get_options_flow(config, options): + def async_get_options_flow(config_entry): class OptionsFlowHandler(data_entry_flow.FlowHandler): - def __init__(self, config, options): - self.config = config - self.options = options - async def async_step_init(self, user_input=None): return self.async_show_form( step_id="finish", data_schema=vol.Schema({"enabled": bool}) @@ -554,7 +546,7 @@ async def test_two_step_options_flow(hass, client): title="Enable disable", data=user_input ) - return OptionsFlowHandler(config, options) + return OptionsFlowHandler() MockConfigEntry( domain="test", @@ -565,7 +557,7 @@ async def test_two_step_options_flow(hass, client): entry = hass.config_entries._entries[0] with patch.dict(HANDLERS, {"test": TestFlow}): - url = "/api/config/config_entries/entry/option/flow" + url = "/api/config/config_entries/options/flow" resp = await client.post(url, json={"handler": entry.entry_id}) assert resp.status == 200 diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index b529ae5b471..361feccc34d 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -666,12 +666,11 @@ async def test_entry_options(hass, manager): class TestFlow: @staticmethod @callback - def async_get_options_flow(config, options): + def async_get_options_flow(config_entry): class OptionsFlowHandler(data_entry_flow.FlowHandler): - def __init__(self, config, options): - pass + pass - return OptionsFlowHandler(config, options) + return OptionsFlowHandler() config_entries.HANDLERS["test"] = TestFlow() flow = await manager.options._async_create_flow( From 1bfe752dfab5b18a5913a07c1afd89352b511ccb Mon Sep 17 00:00:00 2001 From: Keith Pine Date: Thu, 15 Aug 2019 14:22:13 -0700 Subject: [PATCH 145/273] Remove zwave.update_config service call (#25959) The python-openzwave API which provides the implementation of the config update does not actually work in the homeassistant fork. To avoid confusion, remove the service call for now. --- homeassistant/components/zwave/__init__.py | 6 ------ homeassistant/components/zwave/const.py | 1 - homeassistant/components/zwave/services.yaml | 3 --- 3 files changed, 10 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index cd456ede75e..bc40d46b8ba 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -564,11 +564,6 @@ async def async_setup_entry(hass, config_entry): _LOGGER.info("Z-Wave soft_reset have been initialized") network.controller.soft_reset() - def update_config(service): - """Update the config from git.""" - _LOGGER.info("Configuration update has been initialized") - network.controller.update_ozw_config() - def test_network(service): """Test the network by sending commands to all the nodes.""" _LOGGER.info("Z-Wave test_network have been initialized") @@ -891,7 +886,6 @@ async def async_setup_entry(hass, config_entry): hass.services.register(DOMAIN, const.SERVICE_CANCEL_COMMAND, cancel_command) hass.services.register(DOMAIN, const.SERVICE_HEAL_NETWORK, heal_network) hass.services.register(DOMAIN, const.SERVICE_SOFT_RESET, soft_reset) - hass.services.register(DOMAIN, const.SERVICE_UPDATE_CONFIG, update_config) hass.services.register(DOMAIN, const.SERVICE_TEST_NETWORK, test_network) hass.services.register(DOMAIN, const.SERVICE_STOP_NETWORK, stop_network) hass.services.register( diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index d6d1265d09f..83fb43fd3fb 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -68,7 +68,6 @@ SERVICE_RENAME_VALUE = "rename_value" SERVICE_REFRESH_ENTITY = "refresh_entity" SERVICE_REFRESH_NODE = "refresh_node" SERVICE_RESET_NODE_METERS = "reset_node_meters" -SERVICE_UPDATE_CONFIG = "update_config" EVENT_SCENE_ACTIVATED = "zwave.scene_activated" EVENT_NODE_EVENT = "zwave.node_event" diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml index 37b12232759..52b135eba81 100644 --- a/homeassistant/components/zwave/services.yaml +++ b/homeassistant/components/zwave/services.yaml @@ -137,9 +137,6 @@ set_wakeup: value: description: Value of the interval to set. (integer) -update_config: - description: Attempt to update ozw configuration files from git to support newer devices. - start_network: description: Start the Z-Wave network. This might take a while, depending on how big your Z-Wave network is. From a64dccc80b55c1ef9b9ec416558cd428a681e9c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Thu, 15 Aug 2019 23:53:12 +0100 Subject: [PATCH 146/273] edp_redy: remove component (#25971) The service has moved to a new portal and the previous API does not work anymore. --- .coveragerc | 1 - CODEOWNERS | 1 - homeassistant/components/edp_redy/__init__.py | 135 ------------------ .../components/edp_redy/manifest.json | 12 -- homeassistant/components/edp_redy/sensor.py | 115 --------------- homeassistant/components/edp_redy/switch.py | 91 ------------ requirements_all.txt | 3 - 7 files changed, 358 deletions(-) delete mode 100644 homeassistant/components/edp_redy/__init__.py delete mode 100644 homeassistant/components/edp_redy/manifest.json delete mode 100644 homeassistant/components/edp_redy/sensor.py delete mode 100644 homeassistant/components/edp_redy/switch.py diff --git a/.coveragerc b/.coveragerc index 445d3e163ee..248e242faaf 100644 --- a/.coveragerc +++ b/.coveragerc @@ -158,7 +158,6 @@ omit = homeassistant/components/ecovacs/* homeassistant/components/eddystone_temperature/sensor.py homeassistant/components/edimax/switch.py - homeassistant/components/edp_redy/* homeassistant/components/egardia/* homeassistant/components/eight_sleep/* homeassistant/components/eliqonline/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index ff565d79d9d..07501e27f5e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -74,7 +74,6 @@ homeassistant/components/discogs/* @thibmaek homeassistant/components/doorbird/* @oblogic7 homeassistant/components/dweet/* @fabaff homeassistant/components/ecovacs/* @OverloadUT -homeassistant/components/edp_redy/* @abmantis homeassistant/components/egardia/* @jeroenterheerdt homeassistant/components/eight_sleep/* @mezz64 homeassistant/components/elv/* @majuss diff --git a/homeassistant/components/edp_redy/__init__.py b/homeassistant/components/edp_redy/__init__.py deleted file mode 100644 index 8c079078176..00000000000 --- a/homeassistant/components/edp_redy/__init__.py +++ /dev/null @@ -1,135 +0,0 @@ -"""Support for EDP re:dy.""" -from datetime import timedelta -import logging - -import voluptuous as vol - -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_START -from homeassistant.core import callback -from homeassistant.helpers import aiohttp_client, discovery, dispatcher -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_point_in_time -from homeassistant.util import dt as dt_util - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "edp_redy" -EDP_REDY = "edp_redy" -DATA_UPDATE_TOPIC = "{0}_data_update".format(DOMAIN) -UPDATE_INTERVAL = 60 - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass, config): - """Set up the EDP re:dy component.""" - from edp_redy import EdpRedySession - - session = EdpRedySession( - config[DOMAIN][CONF_USERNAME], - config[DOMAIN][CONF_PASSWORD], - aiohttp_client.async_get_clientsession(hass), - hass.loop, - ) - hass.data[EDP_REDY] = session - platform_loaded = False - - async def async_update_and_sched(time): - update_success = await session.async_update() - - if update_success: - nonlocal platform_loaded - # pylint: disable=used-before-assignment - if not platform_loaded: - for component in ["sensor", "switch"]: - await discovery.async_load_platform( - hass, component, DOMAIN, {}, config - ) - platform_loaded = True - - dispatcher.async_dispatcher_send(hass, DATA_UPDATE_TOPIC) - - # schedule next update - async_track_point_in_time( - hass, async_update_and_sched, time + timedelta(seconds=UPDATE_INTERVAL) - ) - - async def start_component(event): - _LOGGER.debug("Starting updates") - await async_update_and_sched(dt_util.utcnow()) - - # only start fetching data after HA boots to prevent delaying the boot - # process - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_component) - - return True - - -class EdpRedyDevice(Entity): - """Representation a base re:dy device.""" - - def __init__(self, session, device_id, name): - """Initialize the device.""" - self._session = session - self._state = None - self._is_available = True - self._device_state_attributes = {} - self._id = device_id - self._unique_id = device_id - self._name = name if name else device_id - - async def async_added_to_hass(self): - """Subscribe to the data updates topic.""" - dispatcher.async_dispatcher_connect( - self.hass, DATA_UPDATE_TOPIC, self._data_updated - ) - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self._unique_id - - @property - def available(self): - """Return True if entity is available.""" - return self._is_available - - @property - def should_poll(self): - """Return the polling state. No polling needed.""" - return False - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._device_state_attributes - - @callback - def _data_updated(self): - """Update state, trigger updates.""" - self.async_schedule_update_ha_state(True) - - def _parse_data(self, data): - """Parse data received from the server.""" - if "OutOfOrder" in data: - try: - self._is_available = not data["OutOfOrder"] - except ValueError: - _LOGGER.error("Could not parse OutOfOrder for %s", self._id) - self._is_available = False diff --git a/homeassistant/components/edp_redy/manifest.json b/homeassistant/components/edp_redy/manifest.json deleted file mode 100644 index 90404b21678..00000000000 --- a/homeassistant/components/edp_redy/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "domain": "edp_redy", - "name": "Edp redy", - "documentation": "https://www.home-assistant.io/components/edp_redy", - "requirements": [ - "edp_redy==0.0.3" - ], - "dependencies": [], - "codeowners": [ - "@abmantis" - ] -} diff --git a/homeassistant/components/edp_redy/sensor.py b/homeassistant/components/edp_redy/sensor.py deleted file mode 100644 index f8fffefb5da..00000000000 --- a/homeassistant/components/edp_redy/sensor.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Support for EDP re:dy sensors.""" -import logging - -from homeassistant.const import POWER_WATT -from homeassistant.helpers.entity import Entity - -from . import EDP_REDY, EdpRedyDevice - -_LOGGER = logging.getLogger(__name__) - -# Load power in watts (W) -ATTR_ACTIVE_POWER = "active_power" - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Perform the setup for re:dy devices.""" - from edp_redy.session import ACTIVE_POWER_ID - - session = hass.data[EDP_REDY] - devices = [] - - # Create sensors for modules - for device_json in session.modules_dict.values(): - if "HA_POWER_METER" not in device_json["Capabilities"]: - continue - devices.append(EdpRedyModuleSensor(session, device_json)) - - # Create a sensor for global active power - devices.append( - EdpRedySensor(session, ACTIVE_POWER_ID, "Power Home", "mdi:flash", POWER_WATT) - ) - - async_add_entities(devices, True) - - -class EdpRedySensor(EdpRedyDevice, Entity): - """Representation of a EDP re:dy generic sensor.""" - - def __init__(self, session, sensor_id, name, icon, unit): - """Initialize the sensor.""" - super().__init__(session, sensor_id, name) - - self._icon = icon - self._unit = unit - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return self._icon - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this sensor.""" - return self._unit - - async def async_update(self): - """Parse the data for this sensor.""" - if self._id in self._session.values_dict: - self._state = self._session.values_dict[self._id] - self._is_available = True - else: - self._is_available = False - - -class EdpRedyModuleSensor(EdpRedyDevice, Entity): - """Representation of a EDP re:dy module sensor.""" - - def __init__(self, session, device_json): - """Initialize the sensor.""" - super().__init__( - session, device_json["PKID"], "Power {0}".format(device_json["Name"]) - ) - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:flash" - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this sensor.""" - return POWER_WATT - - async def async_update(self): - """Parse the data for this sensor.""" - if self._id in self._session.modules_dict: - device_json = self._session.modules_dict[self._id] - self._parse_data(device_json) - else: - self._is_available = False - - def _parse_data(self, data): - """Parse data received from the server.""" - super()._parse_data(data) - - _LOGGER.debug("Sensor data: %s", str(data)) - - for state_var in data["StateVars"]: - if state_var["Name"] == "ActivePower": - try: - self._state = float(state_var["Value"]) * 1000 - except ValueError: - _LOGGER.error("Could not parse power for %s", self._id) - self._state = 0 - self._is_available = False diff --git a/homeassistant/components/edp_redy/switch.py b/homeassistant/components/edp_redy/switch.py deleted file mode 100644 index 18078fab537..00000000000 --- a/homeassistant/components/edp_redy/switch.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Support for EDP re:dy plugs/switches.""" -import logging - -from homeassistant.components.switch import SwitchDevice - -from . import EDP_REDY, EdpRedyDevice - -_LOGGER = logging.getLogger(__name__) - -# Load power in watts (W) -ATTR_ACTIVE_POWER = "active_power" - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Perform the setup for re:dy devices.""" - session = hass.data[EDP_REDY] - devices = [] - for device_json in session.modules_dict.values(): - if "HA_SWITCH" not in device_json["Capabilities"]: - continue - devices.append(EdpRedySwitch(session, device_json)) - - async_add_entities(devices, True) - - -class EdpRedySwitch(EdpRedyDevice, SwitchDevice): - """Representation of a Edp re:dy switch (plugs, switches, etc).""" - - def __init__(self, session, device_json): - """Initialize the switch.""" - super().__init__(session, device_json["PKID"], device_json["Name"]) - - self._active_power = None - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:power-plug" - - @property - def is_on(self): - """Return true if it is on.""" - return self._state - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self._active_power is not None: - attrs = {ATTR_ACTIVE_POWER: self._active_power} - else: - attrs = {} - attrs.update(super().device_state_attributes) - return attrs - - async def async_turn_on(self, **kwargs): - """Turn the switch on.""" - if await self._async_send_state_cmd(True): - self._state = True - self.async_schedule_update_ha_state() - - async def async_turn_off(self, **kwargs): - """Turn the switch off.""" - if await self._async_send_state_cmd(False): - self._state = False - self.async_schedule_update_ha_state() - - async def _async_send_state_cmd(self, state): - state_json = {"devModuleId": self._id, "key": "RelayState", "value": state} - return await self._session.async_set_state_var(state_json) - - async def async_update(self): - """Parse the data for this switch.""" - if self._id in self._session.modules_dict: - device_json = self._session.modules_dict[self._id] - self._parse_data(device_json) - else: - self._is_available = False - - def _parse_data(self, data): - """Parse data received from the server.""" - super()._parse_data(data) - - for state_var in data["StateVars"]: - if state_var["Name"] == "RelayState": - self._state = state_var["Value"] == "true" - elif state_var["Name"] == "ActivePower": - try: - self._active_power = float(state_var["Value"]) * 1000 - except ValueError: - _LOGGER.error("Could not parse power for %s", self._id) - self._active_power = None diff --git a/requirements_all.txt b/requirements_all.txt index b970f95e763..cdf01a607f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -418,9 +418,6 @@ ebusdpy==0.0.16 # homeassistant.components.ecoal_boiler ecoaliface==0.4.0 -# homeassistant.components.edp_redy -edp_redy==0.0.3 - # homeassistant.components.ee_brightbox eebrightbox==0.0.4 From 26c99454a6db67b9e4fd0af5358db755dd0a534d Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 16 Aug 2019 00:59:08 +0200 Subject: [PATCH 147/273] Add netgear_lte.disconnect_lte service (#25967) --- .../components/netgear_lte/__init__.py | 25 +++++++++++-------- .../components/netgear_lte/manifest.json | 2 +- .../components/netgear_lte/services.yaml | 7 ++++++ requirements_all.txt | 2 +- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index 60909ccaee7..e4909ce68fc 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -43,6 +43,7 @@ EVENT_SMS = "netgear_lte_sms" SERVICE_DELETE_SMS = "delete_sms" SERVICE_SET_OPTION = "set_option" SERVICE_CONNECT_LTE = "connect_lte" +SERVICE_DISCONNECT_LTE = "disconnect_lte" ATTR_HOST = "host" ATTR_SMS_ID = "sms_id" @@ -122,6 +123,8 @@ SET_OPTION_SCHEMA = vol.Schema( CONNECT_LTE_SCHEMA = vol.Schema({vol.Optional(ATTR_HOST): cv.string}) +DISCONNECT_LTE_SCHEMA = vol.Schema({vol.Optional(ATTR_HOST): cv.string}) + @attr.s class ModemData: @@ -199,18 +202,20 @@ async def async_setup(hass, config): await modem_data.modem.set_autoconnect_mode(autoconnect) elif service.service == SERVICE_CONNECT_LTE: await modem_data.modem.connect_lte() + elif service.service == SERVICE_DISCONNECT_LTE: + await modem_data.modem.disconnect_lte() - hass.services.async_register( - DOMAIN, SERVICE_DELETE_SMS, service_handler, schema=DELETE_SMS_SCHEMA - ) + service_schemas = { + SERVICE_DELETE_SMS: DELETE_SMS_SCHEMA, + SERVICE_SET_OPTION: SET_OPTION_SCHEMA, + SERVICE_CONNECT_LTE: CONNECT_LTE_SCHEMA, + SERVICE_DISCONNECT_LTE: DISCONNECT_LTE_SCHEMA, + } - hass.services.async_register( - DOMAIN, SERVICE_SET_OPTION, service_handler, schema=SET_OPTION_SCHEMA - ) - - hass.services.async_register( - DOMAIN, SERVICE_CONNECT_LTE, service_handler, schema=CONNECT_LTE_SCHEMA - ) + for service, schema in service_schemas.items(): + hass.services.async_register( + DOMAIN, service, service_handler, schema=schema + ) netgear_lte_config = config[DOMAIN] diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index 12cf81f46bf..8f5db991c76 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -3,7 +3,7 @@ "name": "Netgear lte", "documentation": "https://www.home-assistant.io/components/netgear_lte", "requirements": [ - "eternalegypt==0.0.8" + "eternalegypt==0.0.9" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/netgear_lte/services.yaml b/homeassistant/components/netgear_lte/services.yaml index 4ba3afb07b4..564fb914cf9 100644 --- a/homeassistant/components/netgear_lte/services.yaml +++ b/homeassistant/components/netgear_lte/services.yaml @@ -27,3 +27,10 @@ connect_lte: host: description: The modem that should connect. example: 192.168.5.1 + +disconnect_lte: + description: Ask the modem to close the LTE connection. + fields: + host: + description: The modem that should disconnect. + example: 192.168.5.1 diff --git a/requirements_all.txt b/requirements_all.txt index cdf01a607f1..78aab20d0d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -455,7 +455,7 @@ epson-projector==0.1.3 epsonprinter==0.0.9 # homeassistant.components.netgear_lte -eternalegypt==0.0.8 +eternalegypt==0.0.9 # homeassistant.components.keyboard_remote # evdev==0.6.1 From 90d493a51a042ee8f3ab6cab2736a286eea6d9c7 Mon Sep 17 00:00:00 2001 From: zhumuht <40521367+zhumuht@users.noreply.github.com> Date: Fri, 16 Aug 2019 15:26:11 +0800 Subject: [PATCH 148/273] Add broadlink switch retry time option (#25873) * add broadlink switch retry time in configuration * add broadlink switch retry time in configuration * formatted code by black tool * Change retry default value to 2 to keep the current behavior --- homeassistant/components/broadlink/switch.py | 68 ++++++++++++-------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 4d3fa644f47..277260c0336 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -34,7 +34,9 @@ TIME_BETWEEN_UPDATES = timedelta(seconds=5) DEFAULT_NAME = "Broadlink switch" DEFAULT_TIMEOUT = 10 +DEFAULT_RETRY = 2 CONF_SLOTS = "slots" +CONF_RETRY = "retry" RM_TYPES = [ "rm", @@ -82,6 +84,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_FRIENDLY_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_TYPE, default=SWITCH_TYPES[0]): vol.In(SWITCH_TYPES), vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_RETRY, default=DEFAULT_RETRY): cv.positive_int, } ) @@ -96,6 +99,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): friendly_name = config.get(CONF_FRIENDLY_NAME) mac_addr = binascii.unhexlify(config.get(CONF_MAC).encode().replace(b":", b"")) switch_type = config.get(CONF_TYPE) + retry_times = config.get(CONF_RETRY) def _get_mp1_slot_name(switch_friendly_name, slot): """Get slot name.""" @@ -116,21 +120,26 @@ def setup_platform(hass, config, add_entities, discovery_info=None): broadlink_device, device_config.get(CONF_COMMAND_ON), device_config.get(CONF_COMMAND_OFF), + retry_times, ) ) elif switch_type in SP1_TYPES: broadlink_device = broadlink.sp1((ip_addr, 80), mac_addr, None) - switches = [BroadlinkSP1Switch(friendly_name, broadlink_device)] + switches = [BroadlinkSP1Switch(friendly_name, broadlink_device, retry_times)] elif switch_type in SP2_TYPES: broadlink_device = broadlink.sp2((ip_addr, 80), mac_addr, None) - switches = [BroadlinkSP2Switch(friendly_name, broadlink_device)] + switches = [BroadlinkSP2Switch(friendly_name, broadlink_device, retry_times)] elif switch_type in MP1_TYPES: switches = [] broadlink_device = broadlink.mp1((ip_addr, 80), mac_addr, None) - parent_device = BroadlinkMP1Switch(broadlink_device) + parent_device = BroadlinkMP1Switch(broadlink_device, retry_times) for i in range(1, 5): slot = BroadlinkMP1Slot( - _get_mp1_slot_name(friendly_name, i), broadlink_device, i, parent_device + _get_mp1_slot_name(friendly_name, i), + broadlink_device, + i, + parent_device, + retry_times, ) switches.append(slot) @@ -146,7 +155,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BroadlinkRMSwitch(SwitchDevice, RestoreEntity): """Representation of an Broadlink switch.""" - def __init__(self, name, friendly_name, device, command_on, command_off): + def __init__( + self, name, friendly_name, device, command_on, command_off, retry_times + ): """Initialize the switch.""" self.entity_id = ENTITY_ID_FORMAT.format(slugify(name)) self._name = friendly_name @@ -155,6 +166,8 @@ class BroadlinkRMSwitch(SwitchDevice, RestoreEntity): self._command_off = command_off self._device = device self._is_available = False + self._retry_times = retry_times + _LOGGER.debug("_retry_times : %s", self._retry_times) async def async_added_to_hass(self): """Call when entity about to be added to hass.""" @@ -190,17 +203,17 @@ class BroadlinkRMSwitch(SwitchDevice, RestoreEntity): def turn_on(self, **kwargs): """Turn the device on.""" - if self._sendpacket(self._command_on): + if self._sendpacket(self._command_on, self._retry_times): self._state = True self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" - if self._sendpacket(self._command_off): + if self._sendpacket(self._command_off, self._retry_times): self._state = False self.schedule_update_ha_state() - def _sendpacket(self, packet, retry=2): + def _sendpacket(self, packet, retry): """Send packet to device.""" if packet is None: _LOGGER.debug("Empty packet") @@ -211,12 +224,13 @@ class BroadlinkRMSwitch(SwitchDevice, RestoreEntity): if retry < 1: _LOGGER.error("Error during sending a packet: %s", error) return False - if not self._auth(): + if not self._auth(self._retry_times): return False return self._sendpacket(packet, retry - 1) return True - def _auth(self, retry=2): + def _auth(self, retry): + _LOGGER.debug("_auth : retry=%s", retry) try: auth = self._device.auth() except OSError: @@ -231,14 +245,14 @@ class BroadlinkRMSwitch(SwitchDevice, RestoreEntity): class BroadlinkSP1Switch(BroadlinkRMSwitch): """Representation of an Broadlink switch.""" - def __init__(self, friendly_name, device): + def __init__(self, friendly_name, device, retry_times): """Initialize the switch.""" - super().__init__(friendly_name, friendly_name, device, None, None) + super().__init__(friendly_name, friendly_name, device, None, None, retry_times) self._command_on = 1 self._command_off = 0 self._load_power = None - def _sendpacket(self, packet, retry=2): + def _sendpacket(self, packet, retry): """Send packet to device.""" try: self._device.set_power(packet) @@ -246,7 +260,7 @@ class BroadlinkSP1Switch(BroadlinkRMSwitch): if retry < 1: _LOGGER.error("Error during sending a packet: %s", error) return False - if not self._auth(): + if not self._auth(self._retry_times): return False return self._sendpacket(packet, retry - 1) return True @@ -275,10 +289,11 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): def update(self): """Synchronize state with switch.""" - self._update() + self._update(self._retry_times) - def _update(self, retry=2): + def _update(self, retry): """Update the state of the device.""" + _LOGGER.debug("_update : retry=%s", retry) try: state = self._device.check_power() load_power = self._device.get_energy() @@ -287,7 +302,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): _LOGGER.error("Error during updating the state: %s", error) self._is_available = False return - if not self._auth(): + if not self._auth(self._retry_times): return return self._update(retry - 1) if state is None and retry > 0: @@ -300,9 +315,9 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): class BroadlinkMP1Slot(BroadlinkRMSwitch): """Representation of a slot of Broadlink switch.""" - def __init__(self, friendly_name, device, slot, parent_device): + def __init__(self, friendly_name, device, slot, parent_device, retry_times): """Initialize the slot of switch.""" - super().__init__(friendly_name, friendly_name, device, None, None) + super().__init__(friendly_name, friendly_name, device, None, None, retry_times) self._command_on = 1 self._command_off = 0 self._slot = slot @@ -313,7 +328,7 @@ class BroadlinkMP1Slot(BroadlinkRMSwitch): """Return true if unable to access real state of entity.""" return False - def _sendpacket(self, packet, retry=2): + def _sendpacket(self, packet, retry): """Send packet to device.""" try: self._device.set_power(self._slot, packet) @@ -322,7 +337,7 @@ class BroadlinkMP1Slot(BroadlinkRMSwitch): _LOGGER.error("Error during sending a packet: %s", error) self._is_available = False return False - if not self._auth(): + if not self._auth(self._retry_times): return False return self._sendpacket(packet, max(0, retry - 1)) self._is_available = True @@ -346,10 +361,11 @@ class BroadlinkMP1Slot(BroadlinkRMSwitch): class BroadlinkMP1Switch: """Representation of a Broadlink switch - To fetch states of all slots.""" - def __init__(self, device): + def __init__(self, device, retry_times): """Initialize the switch.""" self._device = device self._states = None + self._retry_times = retry_times def get_outlet_status(self, slot): """Get status of outlet from cached status list.""" @@ -360,9 +376,9 @@ class BroadlinkMP1Switch: @Throttle(TIME_BETWEEN_UPDATES) def update(self): """Fetch new state data for this device.""" - self._update() + self._update(self._retry_times) - def _update(self, retry=2): + def _update(self, retry): """Update the state of the device.""" try: states = self._device.check_power() @@ -370,14 +386,14 @@ class BroadlinkMP1Switch: if retry < 1: _LOGGER.error("Error during updating the state: %s", error) return - if not self._auth(): + if not self._auth(self._retry_times): return return self._update(max(0, retry - 1)) if states is None and retry > 0: return self._update(max(0, retry - 1)) self._states = states - def _auth(self, retry=2): + def _auth(self, retry): """Authenticate the device.""" try: auth = self._device.auth() From 23f26712f0ed740154e7a96ac389b0fa11ec2d4d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 16 Aug 2019 00:29:25 -0700 Subject: [PATCH 149/273] Fix ecobee preset and add climate mode back (#25970) * Fix ecobee preset and add climate mode back * Fix test --- homeassistant/components/ecobee/climate.py | 5 ++++- tests/components/ecobee/test_climate.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index d9af0f93e11..181f1561eba 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -304,7 +304,7 @@ class Thermostat(ClimateDevice): self.vacation = event["name"] return PRESET_VACATION - return None + return self._preset_modes[self.thermostat["program"]["currentClimateRef"]] @property def hvac_mode(self): @@ -357,6 +357,9 @@ class Thermostat(ClimateDevice): status = self.thermostat["equipmentStatus"] return { "fan": self.fan, + "climate_mode": self._preset_modes[ + self.thermostat["program"]["currentClimateRef"] + ], "equipment_running": status, "fan_min_on_time": self.thermostat["settings"]["fanMinOnTime"], } diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 24938e52621..d6c40ddf9ab 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -131,6 +131,7 @@ class TestEcobee(unittest.TestCase): self.ecobee["equipmentStatus"] = "heatPump2" assert { "fan": "off", + "climate_mode": "Climate1", "fan_min_on_time": 10, "equipment_running": "heatPump2", } == self.thermostat.device_state_attributes @@ -138,18 +139,21 @@ class TestEcobee(unittest.TestCase): self.ecobee["equipmentStatus"] = "auxHeat2" assert { "fan": "off", + "climate_mode": "Climate1", "fan_min_on_time": 10, "equipment_running": "auxHeat2", } == self.thermostat.device_state_attributes self.ecobee["equipmentStatus"] = "compCool1" assert { "fan": "off", + "climate_mode": "Climate1", "fan_min_on_time": 10, "equipment_running": "compCool1", } == self.thermostat.device_state_attributes self.ecobee["equipmentStatus"] = "" assert { "fan": "off", + "climate_mode": "Climate1", "fan_min_on_time": 10, "equipment_running": "", } == self.thermostat.device_state_attributes @@ -157,6 +161,15 @@ class TestEcobee(unittest.TestCase): self.ecobee["equipmentStatus"] = "Unknown" assert { "fan": "off", + "climate_mode": "Climate1", + "fan_min_on_time": 10, + "equipment_running": "Unknown", + } == self.thermostat.device_state_attributes + + self.ecobee["program"]["currentClimateRef"] = "c2" + assert { + "fan": "off", + "climate_mode": "Climate2", "fan_min_on_time": 10, "equipment_running": "Unknown", } == self.thermostat.device_state_attributes From 8c419d7228d3a2d1fbb6aaa2ef7440f793f98dfe Mon Sep 17 00:00:00 2001 From: Kareem Straker Date: Fri, 16 Aug 2019 16:13:02 +0200 Subject: [PATCH 150/273] Update pyvera to 0.3.4 (#25986) --- homeassistant/components/vera/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index 07ae7ab3d36..8365ca1a765 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -3,7 +3,7 @@ "name": "Vera", "documentation": "https://www.home-assistant.io/components/vera", "requirements": [ - "pyvera==0.3.3" + "pyvera==0.3.4" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 78aab20d0d0..4263b43f377 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1570,7 +1570,7 @@ pyuptimerobot==0.0.5 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.3.3 +pyvera==0.3.4 # homeassistant.components.vesync pyvesync==1.1.0 From 9b0e1a6620d15c1f40a17bb23e5e66cb9589ec4d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 16 Aug 2019 16:13:40 +0200 Subject: [PATCH 151/273] Upgrade youtube_dl to 2019.08.13 (#25987) --- 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 2c7d63eac2c..f93fe5e77a9 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.07.16" + "youtube_dl==2019.08.13" ], "dependencies": [ "media_player" diff --git a/requirements_all.txt b/requirements_all.txt index 4263b43f377..d1f962cd25c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1959,7 +1959,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.07.16 +youtube_dl==2019.08.13 # homeassistant.components.zengge zengge==0.2 From 1f28d8e74282ddbace489521c20e4bf4a60febc7 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 16 Aug 2019 16:14:32 +0200 Subject: [PATCH 152/273] Upgrade Mastodon.py to 1.4.6 (#25989) --- 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 cfb4353b698..4005e51e373 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.5" + "Mastodon.py==1.4.6" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index d1f962cd25c..5162c0801a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -38,7 +38,7 @@ Adafruit-SHT31==1.0.2 HAP-python==2.5.0 # homeassistant.components.mastodon -Mastodon.py==1.4.5 +Mastodon.py==1.4.6 # homeassistant.components.orangepi_gpio OPi.GPIO==0.3.6 From 7fbdbb773c3d572447b1fdcb1dbee1684524b287 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 16 Aug 2019 16:15:12 +0200 Subject: [PATCH 153/273] Upgrade pytz to >=2019.02 (#25991) --- 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 daac63bee9b..7d9a9fb53df 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ jinja2>=2.10.1 netdisco==2.6.0 pip>=8.0.3 python-slugify==3.0.2 -pytz>=2019.01 +pytz>=2019.02 pyyaml==5.1.1 requests==2.22.0 ruamel.yaml==0.15.99 diff --git a/requirements_all.txt b/requirements_all.txt index 5162c0801a3..423b45d4d3d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -12,7 +12,7 @@ PyJWT==1.7.1 cryptography==2.7 pip>=8.0.3 python-slugify==3.0.2 -pytz>=2019.01 +pytz>=2019.02 pyyaml==5.1.1 requests==2.22.0 ruamel.yaml==0.15.99 diff --git a/setup.py b/setup.py index da50b5f988c..1ba9a3e62e4 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ REQUIRES = [ "cryptography==2.7", "pip>=8.0.3", "python-slugify==3.0.2", - "pytz>=2019.01", + "pytz>=2019.02", "pyyaml==5.1.1", "requests==2.22.0", "ruamel.yaml==0.15.99", From 3307b4421e2d61465b82de4e10d78c5515ec8469 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 16 Aug 2019 16:15:58 +0200 Subject: [PATCH 154/273] Upgrade sqlalchemy to 1.3.7 (#25997) --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 67a426232f2..c91b910724c 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -3,7 +3,7 @@ "name": "Recorder", "documentation": "https://www.home-assistant.io/components/recorder", "requirements": [ - "sqlalchemy==1.3.5" + "sqlalchemy==1.3.7" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 62b591dbe54..a489e3fd736 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -3,7 +3,7 @@ "name": "Sql", "documentation": "https://www.home-assistant.io/components/sql", "requirements": [ - "sqlalchemy==1.3.5" + "sqlalchemy==1.3.7" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7d9a9fb53df..6f35c79039d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ pytz>=2019.02 pyyaml==5.1.1 requests==2.22.0 ruamel.yaml==0.15.99 -sqlalchemy==1.3.5 +sqlalchemy==1.3.7 voluptuous-serialize==2.1.0 voluptuous==0.11.5 zeroconf==0.23.0 diff --git a/requirements_all.txt b/requirements_all.txt index 423b45d4d3d..de397024c3f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1765,7 +1765,7 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.5 +sqlalchemy==1.3.7 # homeassistant.components.srp_energy srpenergy==1.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 85e44ccca44..ae6e9fe01c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -371,7 +371,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.5 +sqlalchemy==1.3.7 # homeassistant.components.srp_energy srpenergy==1.0.6 From 74b6b8a9f6a96bd6e312942737f3e8e7616f2577 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 16 Aug 2019 16:16:42 +0200 Subject: [PATCH 155/273] Upgrade restrictedpython to 4.0 (#25993) --- homeassistant/components/python_script/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/python_script/manifest.json b/homeassistant/components/python_script/manifest.json index 0f88513bb45..610ec92a2b3 100644 --- a/homeassistant/components/python_script/manifest.json +++ b/homeassistant/components/python_script/manifest.json @@ -3,7 +3,7 @@ "name": "Python script", "documentation": "https://www.home-assistant.io/components/python_script", "requirements": [ - "restrictedpython==4.0b8" + "restrictedpython==4.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index de397024c3f..9c3871358de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1624,7 +1624,7 @@ recollect-waste==1.0.1 regenmaschine==1.5.1 # homeassistant.components.python_script -restrictedpython==4.0b8 +restrictedpython==4.0 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae6e9fe01c4..048e9073c51 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -346,7 +346,7 @@ pywebpush==1.9.2 regenmaschine==1.5.1 # homeassistant.components.python_script -restrictedpython==4.0b8 +restrictedpython==4.0 # homeassistant.components.rflink rflink==0.0.46 From 715bb286c478904f1e905cc6452aaae8bef6f800 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 16 Aug 2019 16:17:02 +0200 Subject: [PATCH 156/273] Upgrade shodan to 1.15.0 (#25990) --- homeassistant/components/shodan/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index eeb5f1c5309..7ecc298e3f6 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -3,7 +3,7 @@ "name": "Shodan", "documentation": "https://www.home-assistant.io/components/shodan", "requirements": [ - "shodan==1.13.0" + "shodan==1.15.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 9c3871358de..53c133b1d50 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1690,7 +1690,7 @@ sense_energy==0.7.0 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.13.0 +shodan==1.15.0 # homeassistant.components.simplepush simplepush==1.1.4 From ce35f64d597f68fcd52b558163bdd33a8c8fb316 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 16 Aug 2019 07:17:43 -0700 Subject: [PATCH 157/273] Guard against unavailable climate entities (#25978) --- homeassistant/components/alexa/entities.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index b060d35be90..03d153f5927 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -264,7 +264,9 @@ class ClimateCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" # If we support two modes, one being off, we allow turning on too. - if climate.HVAC_MODE_OFF in self.entity.attributes[climate.ATTR_HVAC_MODES]: + if climate.HVAC_MODE_OFF in self.entity.attributes.get( + climate.ATTR_HVAC_MODES, [] + ): yield AlexaPowerController(self.entity) yield AlexaThermostatController(self.hass, self.entity) From fad54cd6d8eebeaa3aabfa9fba3f3a86cb58ae49 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 16 Aug 2019 17:34:56 +0200 Subject: [PATCH 158/273] Ensure sun conditions are using the right date (#23664) * Ensure sun conditions are using the right date --- homeassistant/helpers/condition.py | 20 ++- tests/components/automation/test_sun.py | 224 ++++++++++++++++++++++++ 2 files changed, 242 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index b8e08e1ac97..40465f83728 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -321,8 +321,24 @@ def sun( before_offset = before_offset or timedelta(0) after_offset = after_offset or timedelta(0) - sunrise = get_astral_event_date(hass, SUN_EVENT_SUNRISE, today) - sunset = get_astral_event_date(hass, SUN_EVENT_SUNSET, today) + sunrise_today = get_astral_event_date(hass, SUN_EVENT_SUNRISE, today) + sunset_today = get_astral_event_date(hass, SUN_EVENT_SUNSET, today) + + sunrise = sunrise_today + sunset = sunset_today + if today > dt_util.as_local( + cast(datetime, sunrise_today) + ).date() and SUN_EVENT_SUNRISE in (before, after): + tomorrow = dt_util.as_local(utcnow + timedelta(days=1)).date() + sunrise_tomorrow = get_astral_event_date(hass, SUN_EVENT_SUNRISE, tomorrow) + sunrise = sunrise_tomorrow + + if today > dt_util.as_local( + cast(datetime, sunset_today) + ).date() and SUN_EVENT_SUNSET in (before, after): + tomorrow = dt_util.as_local(utcnow + timedelta(days=1)).date() + sunset_tomorrow = get_astral_event_date(hass, SUN_EVENT_SUNSET, tomorrow) + sunset = sunset_tomorrow if sunrise is None and SUN_EVENT_SUNRISE in (before, after): # There is no sunrise today diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index e04d1599e4c..2668ac97053 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -617,3 +617,227 @@ async def test_if_action_before_and_after_during(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert 3 == len(calls) + + +async def test_if_action_before_sunrise_no_offset_kotzebue(hass, calls): + """ + Test if action was before sunrise. + + Local timezone: Alaska time + Location: Kotzebue, which has a very skewed local timezone with sunrise + at 7 AM and sunset at 3AM during summer + After sunrise is true from sunrise until midnight, local time. + """ + tz = dt_util.get_time_zone("America/Anchorage") + dt_util.set_default_time_zone(tz) + hass.config.latitude = 66.5 + hass.config.longitude = 162.4 + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": {"condition": "sun", "before": SUN_EVENT_SUNRISE}, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-07-24 07:17:24 local, sunset: 2015-07-25 03:16:27 local + # sunrise: 2015-07-24 15:17:24 UTC, sunset: 2015-07-25 11:16:27 UTC + # now = sunrise + 1s -> 'before sunrise' not true + now = datetime(2015, 7, 24, 15, 17, 25, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 0 == len(calls) + + # now = sunrise -> 'before sunrise' true + now = datetime(2015, 7, 24, 15, 17, 24, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 1 == len(calls) + + # now = local midnight -> 'before sunrise' true + now = datetime(2015, 7, 24, 8, 0, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 2 == len(calls) + + # now = local midnight - 1s -> 'before sunrise' not true + now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 2 == len(calls) + + +async def test_if_action_after_sunrise_no_offset_kotzebue(hass, calls): + """ + Test if action was after sunrise. + + Local timezone: Alaska time + Location: Kotzebue, which has a very skewed local timezone with sunrise + at 7 AM and sunset at 3AM during summer + Before sunrise is true from midnight until sunrise, local time. + """ + tz = dt_util.get_time_zone("America/Anchorage") + dt_util.set_default_time_zone(tz) + hass.config.latitude = 66.5 + hass.config.longitude = 162.4 + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": {"condition": "sun", "after": SUN_EVENT_SUNRISE}, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-07-24 07:17:24 local, sunset: 2015-07-25 03:16:27 local + # sunrise: 2015-07-24 15:17:24 UTC, sunset: 2015-07-25 11:16:27 UTC + # now = sunrise -> 'after sunrise' true + now = datetime(2015, 7, 24, 15, 17, 24, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 1 == len(calls) + + # now = sunrise - 1s -> 'after sunrise' not true + now = datetime(2015, 7, 24, 15, 17, 23, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 1 == len(calls) + + # now = local midnight -> 'after sunrise' not true + now = datetime(2015, 7, 24, 8, 0, 1, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 1 == len(calls) + + # now = local midnight - 1s -> 'after sunrise' true + now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 2 == len(calls) + + +async def test_if_action_before_sunset_no_offset_kotzebue(hass, calls): + """ + Test if action was before sunrise. + + Local timezone: Alaska time + Location: Kotzebue, which has a very skewed local timezone with sunrise + at 7 AM and sunset at 3AM during summer + Before sunset is true from midnight until sunset, local time. + """ + tz = dt_util.get_time_zone("America/Anchorage") + dt_util.set_default_time_zone(tz) + hass.config.latitude = 66.5 + hass.config.longitude = 162.4 + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": {"condition": "sun", "before": SUN_EVENT_SUNSET}, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-07-24 07:17:24 local, sunset: 2015-07-25 03:16:27 local + # sunrise: 2015-07-24 15:17:24 UTC, sunset: 2015-07-25 11:16:27 UTC + # now = sunrise + 1s -> 'before sunrise' not true + now = datetime(2015, 7, 25, 11, 16, 28, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 0 == len(calls) + + # now = sunrise -> 'before sunrise' true + now = datetime(2015, 7, 25, 11, 16, 27, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 1 == len(calls) + + # now = local midnight -> 'before sunrise' true + now = datetime(2015, 7, 24, 8, 0, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 2 == len(calls) + + # now = local midnight - 1s -> 'before sunrise' not true + now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 2 == len(calls) + + +async def test_if_action_after_sunset_no_offset_kotzebue(hass, calls): + """ + Test if action was after sunrise. + + Local timezone: Alaska time + Location: Kotzebue, which has a very skewed local timezone with sunrise + at 7 AM and sunset at 3AM during summer + After sunset is true from sunset until midnight, local time. + """ + tz = dt_util.get_time_zone("America/Anchorage") + dt_util.set_default_time_zone(tz) + hass.config.latitude = 66.5 + hass.config.longitude = 162.4 + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": {"condition": "sun", "after": SUN_EVENT_SUNSET}, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-07-24 07:17:24 local, sunset: 2015-07-25 03:16:27 local + # sunrise: 2015-07-24 15:17:24 UTC, sunset: 2015-07-25 11:16:27 UTC + # now = sunset -> 'after sunset' true + now = datetime(2015, 7, 25, 11, 16, 27, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 1 == len(calls) + + # now = sunset - 1s -> 'after sunset' not true + now = datetime(2015, 7, 25, 11, 16, 26, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 1 == len(calls) + + # now = local midnight -> 'after sunset' not true + now = datetime(2015, 7, 24, 8, 0, 1, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 1 == len(calls) + + # now = local midnight - 1s -> 'after sunset' true + now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 2 == len(calls) From 87f1a8ed0c92886c037bcf190bc622a6fe4ee2c5 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 16 Aug 2019 17:36:57 +0200 Subject: [PATCH 159/273] Upgrade numpy to 1.17.0 (#25998) --- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 6104d4415f9..357bfca607a 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/iqvia", "requirements": [ - "numpy==1.16.4", + "numpy==1.17.0", "pyiqvia==0.2.1" ], "dependencies": [], diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index c740582ebc9..68f14846af7 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -3,7 +3,7 @@ "name": "Opencv", "documentation": "https://www.home-assistant.io/components/opencv", "requirements": [ - "numpy==1.16.4", + "numpy==1.17.0", "opencv-python-headless==4.1.0.25" ], "dependencies": [], diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index c43c38f55d5..9cc06eaee94 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -3,7 +3,7 @@ "name": "Tensorflow", "documentation": "https://www.home-assistant.io/components/tensorflow", "requirements": [ - "numpy==1.16.4", + "numpy==1.17.0", "pillow==5.4.1", "protobuf==3.6.1" ], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 4bf0c0d435e..b9c01c15d20 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -3,7 +3,7 @@ "name": "Trend", "documentation": "https://www.home-assistant.io/components/trend", "requirements": [ - "numpy==1.16.4" + "numpy==1.17.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 53c133b1d50..be8b06b8c24 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -853,7 +853,7 @@ nuheat==0.3.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.16.4 +numpy==1.17.0 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 048e9073c51..9f576d72802 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -221,7 +221,7 @@ netdisco==2.6.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.16.4 +numpy==1.17.0 # homeassistant.components.google oauth2client==4.0.0 From 4c16531df57de91d7dae8f00d2445d0e30769b94 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 16 Aug 2019 18:28:52 +0200 Subject: [PATCH 160/273] Upgrade python-slugify to 3.0.3 (#25995) --- 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 6f35c79039d..29428c99ac4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ importlib-metadata==0.18 jinja2>=2.10.1 netdisco==2.6.0 pip>=8.0.3 -python-slugify==3.0.2 +python-slugify==3.0.3 pytz>=2019.02 pyyaml==5.1.1 requests==2.22.0 diff --git a/requirements_all.txt b/requirements_all.txt index be8b06b8c24..69e49e12396 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -11,7 +11,7 @@ jinja2>=2.10.1 PyJWT==1.7.1 cryptography==2.7 pip>=8.0.3 -python-slugify==3.0.2 +python-slugify==3.0.3 pytz>=2019.02 pyyaml==5.1.1 requests==2.22.0 diff --git a/setup.py b/setup.py index 1ba9a3e62e4..ce745ac4de9 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ REQUIRES = [ # PyJWT has loose dependency. We want the latest one. "cryptography==2.7", "pip>=8.0.3", - "python-slugify==3.0.2", + "python-slugify==3.0.3", "pytz>=2019.02", "pyyaml==5.1.1", "requests==2.22.0", From 1be6a7b7ee34d205278504836d284de0192eee10 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 16 Aug 2019 20:02:24 +0200 Subject: [PATCH 161/273] Upgrade importlib-metadata to 0.19 (#26003) --- 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 29428c99ac4..5ee1567754f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.7 distro==1.4.0 hass-nabucasa==0.16 home-assistant-frontend==20190815.0 -importlib-metadata==0.18 +importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 pip>=8.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 69e49e12396..b6790203306 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,7 +6,7 @@ attrs==19.1.0 bcrypt==3.1.7 certifi>=2019.6.16 contextvars==2.4;python_version<"3.7" -importlib-metadata==0.18 +importlib-metadata==0.19 jinja2>=2.10.1 PyJWT==1.7.1 cryptography==2.7 diff --git a/setup.py b/setup.py index ce745ac4de9..5e609a20f51 100755 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ REQUIRES = [ "bcrypt==3.1.7", "certifi>=2019.6.16", 'contextvars==2.4;python_version<"3.7"', - "importlib-metadata==0.18", + "importlib-metadata==0.19", "jinja2>=2.10.1", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. From 5ca6c990cf495d0bb5dacfccc574b5a527ce164f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 16 Aug 2019 20:29:38 +0200 Subject: [PATCH 162/273] UniFi - add ap_mac to attributes (#26004) --- homeassistant/components/unifi/device_tracker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 42a6f496a2a..491a032e1cc 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -41,6 +41,7 @@ LOGGER = logging.getLogger(__name__) DEVICE_ATTRIBUTES = [ "_is_guest_by_uap", + "ap_mac", "authorized", "essid", "hostname", From 2a2f7626de20ec0651337e3c8123a3066d8f5ce1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 16 Aug 2019 21:31:28 +0200 Subject: [PATCH 163/273] Upgrade voluptuous to 0.11.7 (#26007) --- 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 5ee1567754f..bf9d9f2324b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ requests==2.22.0 ruamel.yaml==0.15.99 sqlalchemy==1.3.7 voluptuous-serialize==2.1.0 -voluptuous==0.11.5 +voluptuous==0.11.7 zeroconf==0.23.0 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index b6790203306..81ff420a83a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -16,7 +16,7 @@ pytz>=2019.02 pyyaml==5.1.1 requests==2.22.0 ruamel.yaml==0.15.99 -voluptuous==0.11.5 +voluptuous==0.11.7 voluptuous-serialize==2.1.0 # homeassistant.components.nuimo_controller diff --git a/setup.py b/setup.py index 5e609a20f51..f7d5ba73071 100755 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ REQUIRES = [ "pyyaml==5.1.1", "requests==2.22.0", "ruamel.yaml==0.15.99", - "voluptuous==0.11.5", + "voluptuous==0.11.7", "voluptuous-serialize==2.1.0", ] From b43ef6c0cfe35f2094536e68b7fe90bbc7893272 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 16 Aug 2019 22:48:44 +0200 Subject: [PATCH 164/273] Upgrade pyyaml to 5.1.2 (#25994) --- 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 bf9d9f2324b..165cea59a03 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ netdisco==2.6.0 pip>=8.0.3 python-slugify==3.0.3 pytz>=2019.02 -pyyaml==5.1.1 +pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.99 sqlalchemy==1.3.7 diff --git a/requirements_all.txt b/requirements_all.txt index 81ff420a83a..42cb241fdac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -13,7 +13,7 @@ cryptography==2.7 pip>=8.0.3 python-slugify==3.0.3 pytz>=2019.02 -pyyaml==5.1.1 +pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.99 voluptuous==0.11.7 diff --git a/setup.py b/setup.py index f7d5ba73071..148d8a7992f 100755 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ REQUIRES = [ "pip>=8.0.3", "python-slugify==3.0.3", "pytz>=2019.02", - "pyyaml==5.1.1", + "pyyaml==5.1.2", "requests==2.22.0", "ruamel.yaml==0.15.99", "voluptuous==0.11.7", From e21a677239c251de407c86b4b8efd05f7caa052b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 16 Aug 2019 22:49:28 +0200 Subject: [PATCH 165/273] Upgrade pillow to 6.1.0 (#26005) --- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index a4a33efa2cd..54d5ebe5f14 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -3,7 +3,7 @@ "name": "Proxy", "documentation": "https://www.home-assistant.io/components/proxy", "requirements": [ - "pillow==5.4.1" + "pillow==6.1.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 96a351ac453..eb8da25bace 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -3,7 +3,7 @@ "name": "Qrcode", "documentation": "https://www.home-assistant.io/components/qrcode", "requirements": [ - "pillow==5.4.1", + "pillow==6.1.0", "pyzbar==0.1.7" ], "dependencies": [], diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 9cc06eaee94..9cbd349addc 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/components/tensorflow", "requirements": [ "numpy==1.17.0", - "pillow==5.4.1", + "pillow==6.1.0", "protobuf==3.6.1" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index 42cb241fdac..0d9607d8d41 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -928,7 +928,7 @@ pilight==0.1.1 # homeassistant.components.proxy # homeassistant.components.qrcode # homeassistant.components.tensorflow -pillow==5.4.1 +pillow==6.1.0 # homeassistant.components.dominos pizzapi==0.0.3 From 9e5243929e3821daa44ecb592406fa4a4da88cd1 Mon Sep 17 00:00:00 2001 From: OliverRepo <54188537+OliverRepo@users.noreply.github.com> Date: Fri, 16 Aug 2019 23:47:21 +0200 Subject: [PATCH 166/273] Fix bmw_connected_drive and eq3btsmart components by updating their dependencies (#26012) * Fix bmw_connected_drive by upgrading bimmer_connected module depencies * Fix broken eq3btsmart by upgrading python-eq3bt module dependcies * Run Development Checklist --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- homeassistant/components/eq3btsmart/manifest.json | 2 +- requirements_all.txt | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) mode change 100644 => 100755 homeassistant/components/bmw_connected_drive/manifest.json mode change 100644 => 100755 homeassistant/components/eq3btsmart/manifest.json diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json old mode 100644 new mode 100755 index eec81aa6525..ad5f712f817 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -3,7 +3,7 @@ "name": "Bmw connected drive", "documentation": "https://www.home-assistant.io/components/bmw_connected_drive", "requirements": [ - "bimmer_connected==0.5.3" + "bimmer_connected==0.5.6" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json old mode 100644 new mode 100755 index 6d13c79bcec..26d732fc927 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/components/eq3btsmart", "requirements": [ "construct==2.9.45", - "python-eq3bt==0.1.9" + "python-eq3bt==0.1.11" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 0d9607d8d41..880a95566fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -266,7 +266,7 @@ beautifulsoup4==4.7.1 bellows-homeassistant==0.9.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.5.3 +bimmer_connected==0.5.6 # homeassistant.components.bizkaibus bizkaibus==0.1.1 @@ -1446,7 +1446,7 @@ python-digitalocean==1.13.2 python-ecobee-api==0.0.21 # homeassistant.components.eq3btsmart -# python-eq3bt==0.1.9 +# python-eq3bt==0.1.11 # homeassistant.components.etherscan python-etherscan-api==0.0.3 From b5061939c75ab9223e8bc1c50c57e56079c9084d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 17 Aug 2019 01:15:44 +0200 Subject: [PATCH 167/273] Upgrade luftdaten to 0.6.3 (#26009) --- homeassistant/components/luftdaten/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json index a29c7faa06a..26d6c21f3a9 100644 --- a/homeassistant/components/luftdaten/manifest.json +++ b/homeassistant/components/luftdaten/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/luftdaten", "requirements": [ - "luftdaten==0.6.2" + "luftdaten==0.6.3" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 880a95566fd..548ed65f12f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -753,7 +753,7 @@ logi_circle==0.2.2 london-tube-status==0.2 # homeassistant.components.luftdaten -luftdaten==0.6.2 +luftdaten==0.6.3 # homeassistant.components.lupusec lupupy==0.0.17 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f576d72802..b9ce85b1297 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -205,7 +205,7 @@ libpurecool==0.5.0 libsoundtouch==0.7.2 # homeassistant.components.luftdaten -luftdaten==0.6.2 +luftdaten==0.6.3 # homeassistant.components.mythicbeastsdns mbddns==0.1.2 From 2fdbb3b4df9f006c938b67082d472012928f0477 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 17 Aug 2019 01:16:04 +0200 Subject: [PATCH 168/273] Upgrade pysnmp to 4.4.11 (#26010) --- homeassistant/components/snmp/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index 0007a5a66e5..a3ac3af985d 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -3,7 +3,7 @@ "name": "Snmp", "documentation": "https://www.home-assistant.io/components/snmp", "requirements": [ - "pysnmp==4.4.9" + "pysnmp==4.4.11" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 548ed65f12f..85892e5b830 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1398,7 +1398,7 @@ pysmartthings==0.6.9 pysmarty==0.8 # homeassistant.components.snmp -pysnmp==4.4.9 +pysnmp==4.4.11 # homeassistant.components.sonos pysonos==0.0.23 From b5893a8a6e970600d4f50d6404e99e1cdd44a735 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 17 Aug 2019 01:16:46 +0200 Subject: [PATCH 169/273] Upgrade beautifulsoup4 to 4.8.0 (#26006) --- homeassistant/components/linksys_ap/manifest.json | 2 +- homeassistant/components/scrape/manifest.json | 2 +- homeassistant/components/sytadin/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/linksys_ap/manifest.json b/homeassistant/components/linksys_ap/manifest.json index ccad7298d6b..31fafe17edd 100644 --- a/homeassistant/components/linksys_ap/manifest.json +++ b/homeassistant/components/linksys_ap/manifest.json @@ -3,7 +3,7 @@ "name": "Linksys ap", "documentation": "https://www.home-assistant.io/components/linksys_ap", "requirements": [ - "beautifulsoup4==4.7.1" + "beautifulsoup4==4.8.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index c7e60140dbf..ec9807d4e00 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -3,7 +3,7 @@ "name": "Scrape", "documentation": "https://www.home-assistant.io/components/scrape", "requirements": [ - "beautifulsoup4==4.7.1" + "beautifulsoup4==4.8.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/sytadin/manifest.json b/homeassistant/components/sytadin/manifest.json index 0efc84fc552..c1453d88d81 100644 --- a/homeassistant/components/sytadin/manifest.json +++ b/homeassistant/components/sytadin/manifest.json @@ -3,7 +3,7 @@ "name": "Sytadin", "documentation": "https://www.home-assistant.io/components/sytadin", "requirements": [ - "beautifulsoup4==4.7.1" + "beautifulsoup4==4.8.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 85892e5b830..f6bfb80d480 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -260,7 +260,7 @@ batinfo==0.4.2 # homeassistant.components.linksys_ap # homeassistant.components.scrape # homeassistant.components.sytadin -beautifulsoup4==4.7.1 +beautifulsoup4==4.8.0 # homeassistant.components.zha bellows-homeassistant==0.9.0 From 6c292846be204ec5f69a2cb7ac183f02fbe5c775 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 16 Aug 2019 16:17:16 -0700 Subject: [PATCH 170/273] Allow entities to indicate they should be disabled by default (#26011) --- homeassistant/helpers/entity.py | 5 +++++ homeassistant/helpers/entity_platform.py | 6 ++++++ homeassistant/helpers/entity_registry.py | 7 ++++++- tests/common.py | 5 +++++ tests/helpers/test_entity_platform.py | 19 +++++++++++++++++++ tests/helpers/test_entity_registry.py | 14 ++++++++++++++ 6 files changed, 55 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 6508de08143..aecdf45dde5 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -216,6 +216,11 @@ class Entity: """Time that a context is considered recent.""" return timedelta(seconds=5) + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + return True + # DO NOT OVERWRITE # These properties and methods are either managed by Home Assistant or they # are used to perform a very specific function. Overwriting these may diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index ea71828f21a..dd19fac05c8 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -8,6 +8,7 @@ from homeassistant.core import callback, valid_entity_id, split_entity_id from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.util.async_ import run_callback_threadsafe, run_coroutine_threadsafe +from .entity_registry import DISABLED_INTEGRATION from .event import async_track_time_interval, async_call_later @@ -333,6 +334,10 @@ class EntityPlatform: if device: device_id = device.id + disabled_by: Optional[str] = None + if not entity.entity_registry_enabled_default: + disabled_by = DISABLED_INTEGRATION + entry = entity_registry.async_get_or_create( self.domain, self.platform_name, @@ -341,6 +346,7 @@ class EntityPlatform: config_entry_id=config_entry_id, device_id=device_id, known_object_ids=self.entities.keys(), + disabled_by=disabled_by, ) if entry.disabled: diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 9a7be9ecc36..97cc213aa66 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -35,6 +35,7 @@ _LOGGER = logging.getLogger(__name__) _UNDEF = object() DISABLED_HASS = "hass" DISABLED_USER = "user" +DISABLED_INTEGRATION = "integration" STORAGE_VERSION = 1 STORAGE_KEY = "core.entity_registry" @@ -53,7 +54,9 @@ class RegistryEntry: disabled_by = attr.ib( type=str, default=None, - validator=attr.validators.in_((DISABLED_HASS, DISABLED_USER, None)), + validator=attr.validators.in_( + (DISABLED_HASS, DISABLED_USER, DISABLED_INTEGRATION, None) + ), ) # type: Optional[str] domain = attr.ib(type=str, init=False, repr=False) @@ -132,6 +135,7 @@ class EntityRegistry: config_entry_id=None, device_id=None, known_object_ids=None, + disabled_by=None, ): """Get entity. Create if it doesn't exist.""" entity_id = self.async_get_entity_id(domain, platform, unique_id) @@ -161,6 +165,7 @@ class EntityRegistry: device_id=device_id, unique_id=unique_id, platform=platform, + disabled_by=disabled_by, ) self.entities[entity_id] = entity _LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id) diff --git a/tests/common.py b/tests/common.py index a139ca83743..f7816bf2192 100644 --- a/tests/common.py +++ b/tests/common.py @@ -908,6 +908,11 @@ class MockEntity(entity.Entity): """Info how it links to a device.""" return self._handle("device_info") + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + return self._handle("entity_registry_enabled_default") + def _handle(self, attr): """Return attribute value.""" if attr in self._values: diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 0f43c6ab4aa..606a4c82096 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -775,3 +775,22 @@ async def test_device_info_not_overrides(hass): assert device.id == device2.id assert device2.manufacturer == "test-manufacturer" assert device2.model == "test-model" + + +async def test_entity_disabled_by_integration(hass): + """Test entity disabled by integration.""" + component = EntityComponent(_LOGGER, DOMAIN, hass, timedelta(seconds=20)) + + entity_default = MockEntity(unique_id="default") + entity_disabled = MockEntity( + unique_id="disabled", entity_registry_enabled_default=False + ) + + await component.async_add_entities([entity_default, entity_disabled]) + + registry = await hass.helpers.entity_registry.async_get_registry() + + entry_default = registry.async_get_or_create(DOMAIN, DOMAIN, "default") + assert entry_default.disabled_by is None + entry_disabled = registry.async_get_or_create(DOMAIN, DOMAIN, "disabled") + assert entry_disabled.disabled_by == "integration" diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index a3ffcb4d1ff..88131a58de0 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -352,3 +352,17 @@ async def test_update_entity_unique_id_conflict(registry): ) as mock_schedule_save, pytest.raises(ValueError): registry.async_update_entity(entry.entity_id, new_unique_id=entry2.unique_id) assert mock_schedule_save.call_count == 0 + + +async def test_disabled_by(registry): + """Test that we can disable an entry when we create it.""" + entry = registry.async_get_or_create("light", "hue", "5678", disabled_by="hass") + assert entry.disabled_by == "hass" + + entry = registry.async_get_or_create( + "light", "hue", "5678", disabled_by="integration" + ) + assert entry.disabled_by == "hass" + + entry2 = registry.async_get_or_create("light", "hue", "1234") + assert entry2.disabled_by is None From 57ef721d5da705d2888e1980f210dddd02684f4d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 16 Aug 2019 16:19:00 -0700 Subject: [PATCH 171/273] Hue tweak registered device type + discovery exception (#25977) * Include location name in create user * Guard against no host in context * Fix tests and typing --- homeassistant/auth/__init__.py | 2 +- homeassistant/components/hue/bridge.py | 2 +- homeassistant/components/hue/config_flow.py | 14 ++++++++------ homeassistant/data_entry_flow.py | 2 +- tests/components/hue/test_config_flow.py | 5 +++++ tests/helpers/test_config_entry_flow.py | 2 ++ 6 files changed, 18 insertions(+), 9 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index f00687b828c..2641f0b8f7e 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -458,7 +458,7 @@ class AuthManager: result["data"] ) - if flow.context is not None and flow.context.get("credential_only"): + if flow.context.get("credential_only"): result["result"] = credentials return result diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index d11b38dd69c..6a654744397 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -166,7 +166,7 @@ async def get_bridge(hass, host, username=None): with async_timeout.timeout(10): # Create username if we don't have one if not username: - await bridge.create_user("home-assistant") + await bridge.create_user(f"home-assistant#{hass.config.location_name}") # Initialize bridge (and validate our username) await bridge.initialize() diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 829ea814d27..1d058d84b61 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -62,7 +62,7 @@ class HueFlowHandler(config_entries.ConfigFlow): async def async_step_init(self, user_input=None): """Handle a flow start.""" if user_input is not None: - self.host = user_input["host"] + self.host = self.context["host"] = user_input["host"] return await self.async_step_link() websession = aiohttp_client.async_get_clientsession(self.hass) @@ -141,10 +141,11 @@ class HueFlowHandler(config_entries.ConfigFlow): if "HASS Bridge" in discovery_info.get("name", ""): return self.async_abort(reason="already_configured") - # 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()): + if any( + host == flow["context"].get("host") for flow in self._async_in_progress() + ): return self.async_abort(reason="already_in_progress") if host in configured_hosts(self.hass): @@ -166,10 +167,11 @@ class HueFlowHandler(config_entries.ConfigFlow): async def async_step_homekit(self, homekit_info): """Handle HomeKit discovery.""" - # pylint: disable=unsupported-assignment-operation host = self.context["host"] = homekit_info.get("host") - if any(host == flow["context"]["host"] for flow in self._async_in_progress()): + if any( + host == flow["context"].get("host") for flow in self._async_in_progress() + ): return self.async_abort(reason="already_in_progress") if host in configured_hosts(self.hass): @@ -192,7 +194,7 @@ class HueFlowHandler(config_entries.ConfigFlow): and create an entry. Otherwise we will delegate to `link` step which will ask user to link the bridge. """ - host = import_info["host"] + host = self.context["host"] = import_info["host"] path = import_info.get("path") if path is not None: diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 7ce27d404cf..0af6677dceb 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -179,7 +179,7 @@ class FlowHandler: hass: Optional[HomeAssistant] = None handler = None cur_step: Optional[Dict[str, str]] = None - context: Optional[Dict] = None + context: Dict # Set by _async_create_flow callback init_step = "init" diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 2228c2dcfbf..54082464a7c 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -255,6 +255,7 @@ async def test_import_with_existing_config(hass): """Test importing a host with an existing config file.""" flow = config_flow.HueFlowHandler() flow.hass = hass + flow.context = {} bridge = Mock() bridge.username = "username-abc" @@ -280,6 +281,7 @@ async def test_import_with_no_config(hass): """Test importing a host without an existing config file.""" flow = config_flow.HueFlowHandler() flow.hass = hass + flow.context = {} with patch.object( config_flow, "get_bridge", side_effect=errors.AuthenticationRequired @@ -294,6 +296,7 @@ async def test_import_with_existing_but_invalid_config(hass): """Test importing a host with a config file with invalid username.""" flow = config_flow.HueFlowHandler() flow.hass = hass + flow.context = {} with patch.object( config_flow, "_find_username_from_config", return_value="mock-user" @@ -310,6 +313,7 @@ async def test_import_cannot_connect(hass): """Test importing a host that we cannot conncet to.""" flow = config_flow.HueFlowHandler() flow.hass = hass + flow.context = {} with patch.object(config_flow, "get_bridge", side_effect=errors.CannotConnect): result = await flow.async_step_import({"host": "0.0.0.0"}) @@ -337,6 +341,7 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): flow = config_flow.HueFlowHandler() flow.hass = hass + flow.context = {} bridge = Mock() bridge.username = "username-abc" diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 7bce0d69c51..3c3d1224e12 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -80,6 +80,7 @@ async def test_discovery_single_instance(hass, discovery_flow_conf, source): """Test we not allow duplicates.""" flow = config_entries.HANDLERS["test"]() flow.hass = hass + flow.context = {} MockConfigEntry(domain="test").add_to_hass(hass) result = await getattr(flow, "async_step_{}".format(source))({}) @@ -93,6 +94,7 @@ 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 + flow.context = {} result = await getattr(flow, "async_step_{}".format(source))({}) From 8b66c11706c1d76c23671953d90c95030b08e22b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 16 Aug 2019 16:19:19 -0700 Subject: [PATCH 172/273] Fix config entry has options check (#25976) * Fix config entry has options check * Register webhook/discovery config flows with classes * Fix types * Apply suggestions from code review Co-Authored-By: Martin Hjelmare --- .../components/config/config_entries.py | 25 ++-- homeassistant/helpers/config_entry_flow.py | 69 +++++++---- .../components/config/test_config_entries.py | 108 +++++++++--------- 3 files changed, 115 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 68967439b2a..21fc55ebafc 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -54,8 +54,18 @@ class ConfigManagerEntryIndexView(HomeAssistantView): """List available config entries.""" hass = request.app["hass"] - return self.json( - [ + results = [] + + for entry in hass.config_entries.async_entries(): + handler = config_entries.HANDLERS.get(entry.domain) + supports_options = ( + # Guard in case handler is no longer registered (custom compnoent etc) + handler is not None + # pylint: disable=comparison-with-callable + and handler.async_get_options_flow + != config_entries.ConfigFlow.async_get_options_flow + ) + results.append( { "entry_id": entry.entry_id, "domain": entry.domain, @@ -63,14 +73,11 @@ class ConfigManagerEntryIndexView(HomeAssistantView): "source": entry.source, "state": entry.state, "connection_class": entry.connection_class, - "supports_options": hasattr( - config_entries.HANDLERS.get(entry.domain), - "async_get_options_flow", - ), + "supports_options": supports_options, } - for entry in hass.config_entries.async_entries() - ] - ) + ) + + return self.json(results) class ConfigManagerEntryResourceView(HomeAssistantView): diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index f341cc2ce02..922878fb324 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -1,29 +1,11 @@ """Helpers for data entry flows for config entries.""" -from functools import partial - +from typing import Callable, Awaitable, Union from homeassistant import config_entries from .typing import HomeAssistantType - # mypy: allow-untyped-defs - -def register_discovery_flow(domain, title, discovery_function, connection_class): - """Register flow for discovered integrations that not require auth.""" - config_entries.HANDLERS.register(domain)( - partial( - DiscoveryFlowHandler, domain, title, discovery_function, connection_class - ) - ) - - -def register_webhook_flow(domain, title, description_placeholder, allow_multiple=False): - """Register flow for webhook integrations.""" - config_entries.HANDLERS.register(domain)( - partial( - WebhookFlowHandler, domain, title, description_placeholder, allow_multiple - ) - ) +DiscoveryFunctionType = Callable[[], Union[Awaitable[bool], bool]] class DiscoveryFlowHandler(config_entries.ConfigFlow): @@ -31,7 +13,13 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): VERSION = 1 - def __init__(self, domain, title, discovery_function, connection_class): + def __init__( + self, + domain: str, + title: str, + discovery_function: DiscoveryFunctionType, + connection_class: str, + ) -> None: """Initialize the discovery config flow.""" self._domain = domain self._title = title @@ -91,12 +79,35 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): return self.async_create_entry(title=self._title, data={}) +def register_discovery_flow( + domain: str, + title: str, + discovery_function: DiscoveryFunctionType, + connection_class: str, +) -> None: + """Register flow for discovered integrations that not require auth.""" + + class DiscoveryFlow(DiscoveryFlowHandler): + """Discovery flow handler.""" + + def __init__(self) -> None: + super().__init__(domain, title, discovery_function, connection_class) + + config_entries.HANDLERS.register(domain)(DiscoveryFlow) + + class WebhookFlowHandler(config_entries.ConfigFlow): """Handle a webhook config flow.""" VERSION = 1 - def __init__(self, domain, title, description_placeholder, allow_multiple): + def __init__( + self, + domain: str, + title: str, + description_placeholder: dict, + allow_multiple: bool, + ) -> None: """Initialize the discovery config flow.""" self._domain = domain self._title = title @@ -131,6 +142,20 @@ class WebhookFlowHandler(config_entries.ConfigFlow): ) +def register_webhook_flow( + domain: str, title: str, description_placeholder: dict, allow_multiple: bool = False +) -> None: + """Register flow for webhook integrations.""" + + class WebhookFlow(WebhookFlowHandler): + """Webhook flow handler.""" + + def __init__(self) -> None: + super().__init__(domain, title, description_placeholder, allow_multiple) + + config_entries.HANDLERS.register(domain)(WebhookFlow) + + async def webhook_async_remove_entry( hass: HomeAssistantType, entry: config_entries.ConfigEntry ) -> None: diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index efe476b7055..13cd8da0597 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -37,65 +37,61 @@ def client(hass, hass_client): yield hass.loop.run_until_complete(hass_client()) -@HANDLERS.register("comp1") -class Comp1ConfigFlow: - """Config flow with options flow.""" - - @staticmethod - @callback - def async_get_options_flow(config, options): - """Get options flow.""" - pass - - -@HANDLERS.register("comp2") -class Comp2ConfigFlow: - """Config flow without options flow.""" - - def __init__(self): - """Init.""" - pass - - async def test_get_entries(hass, client): """Test get entries.""" - MockConfigEntry( - domain="comp1", - title="Test 1", - source="bla", - connection_class=core_ce.CONN_CLASS_LOCAL_POLL, - ).add_to_hass(hass) - MockConfigEntry( - domain="comp2", - title="Test 2", - source="bla2", - state=core_ce.ENTRY_STATE_LOADED, - connection_class=core_ce.CONN_CLASS_ASSUMED, - ).add_to_hass(hass) + with patch.dict(HANDLERS, clear=True): - resp = await client.get("/api/config/config_entries/entry") - assert resp.status == 200 - data = await resp.json() - for entry in data: - entry.pop("entry_id") - assert data == [ - { - "domain": "comp1", - "title": "Test 1", - "source": "bla", - "state": "not_loaded", - "connection_class": "local_poll", - "supports_options": True, - }, - { - "domain": "comp2", - "title": "Test 2", - "source": "bla2", - "state": "loaded", - "connection_class": "assumed", - "supports_options": False, - }, - ] + @HANDLERS.register("comp1") + class Comp1ConfigFlow: + """Config flow with options flow.""" + + @staticmethod + @callback + def async_get_options_flow(config, options): + """Get options flow.""" + pass + + hass.helpers.config_entry_flow.register_discovery_flow( + "comp2", "Comp 2", lambda: None, core_ce.CONN_CLASS_ASSUMED + ) + + MockConfigEntry( + domain="comp1", + title="Test 1", + source="bla", + connection_class=core_ce.CONN_CLASS_LOCAL_POLL, + ).add_to_hass(hass) + MockConfigEntry( + domain="comp2", + title="Test 2", + source="bla2", + state=core_ce.ENTRY_STATE_LOADED, + connection_class=core_ce.CONN_CLASS_ASSUMED, + ).add_to_hass(hass) + + resp = await client.get("/api/config/config_entries/entry") + assert resp.status == 200 + data = await resp.json() + for entry in data: + entry.pop("entry_id") + assert data == [ + { + "domain": "comp1", + "title": "Test 1", + "source": "bla", + "state": "not_loaded", + "connection_class": "local_poll", + "supports_options": True, + }, + { + "domain": "comp2", + "title": "Test 2", + "source": "bla2", + "state": "loaded", + "connection_class": "assumed", + "supports_options": False, + }, + ] @asyncio.coroutine From d4046cb6e41aaa578290d0b43f83439b920e4955 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 17 Aug 2019 01:22:12 +0200 Subject: [PATCH 173/273] Update KNX services.yaml (#26014) Fixes wrong (outdated?) services.yaml for knx integration. --- homeassistant/components/knx/services.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml index 79b11c129af..5b751bac17c 100644 --- a/homeassistant/components/knx/services.yaml +++ b/homeassistant/components/knx/services.yaml @@ -1,5 +1,9 @@ -group_write: - description: Turn a light on. +send: + description: "Send arbitrary data directly to the KNX bus." fields: - address: {description: Group address(es) to write to., example: 1/1/0} - data: {description: KNX data to send., example: 1} + address: + description: "Group address(es) to write to." + example: "1/1/0" + payload: + description: "Payload to send to the bus. Integers are treated as DPT 1/2/3 payloads. For DPTs > 6 bits send a list. Each value represents 1 octet (0-255). Pad with 0 to DPT byte length." + example: "[0, 4]" From eba6caf8a17d7c3f6f5a725c459f7ea9252a1919 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 16 Aug 2019 16:22:45 -0700 Subject: [PATCH 174/273] Entity registry api update disable (#26015) * Clean up entity registry WS commands * Allow updating disabled_by in entity registry * Allow changing disabled_by via API * Update tests/components/config/test_entity_registry.py Co-Authored-By: Robert Svensson --- .../components/config/entity_registry.py | 69 +++++++++---------- homeassistant/helpers/entity_registry.py | 26 ++++--- .../components/config/test_entity_registry.py | 35 ++++++++-- tests/helpers/test_entity_registry.py | 20 ++++++ 4 files changed, 97 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 431723893c1..125b2260f08 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -11,51 +11,18 @@ from homeassistant.components.websocket_api.decorators import ( ) from homeassistant.helpers import config_validation as cv -WS_TYPE_LIST = "config/entity_registry/list" -SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_LIST} -) - -WS_TYPE_GET = "config/entity_registry/get" -SCHEMA_WS_GET = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_GET, vol.Required("entity_id"): cv.entity_id} -) - -WS_TYPE_UPDATE = "config/entity_registry/update" -SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - { - vol.Required("type"): WS_TYPE_UPDATE, - vol.Required("entity_id"): cv.entity_id, - # If passed in, we update value. Passing None will remove old value. - vol.Optional("name"): vol.Any(str, None), - vol.Optional("new_entity_id"): str, - } -) - -WS_TYPE_REMOVE = "config/entity_registry/remove" -SCHEMA_WS_REMOVE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_REMOVE, vol.Required("entity_id"): cv.entity_id} -) - async def async_setup(hass): """Enable the Entity Registry views.""" - hass.components.websocket_api.async_register_command( - WS_TYPE_LIST, websocket_list_entities, SCHEMA_WS_LIST - ) - hass.components.websocket_api.async_register_command( - WS_TYPE_GET, websocket_get_entity, SCHEMA_WS_GET - ) - hass.components.websocket_api.async_register_command( - WS_TYPE_UPDATE, websocket_update_entity, SCHEMA_WS_UPDATE - ) - hass.components.websocket_api.async_register_command( - WS_TYPE_REMOVE, websocket_remove_entity, SCHEMA_WS_REMOVE - ) + hass.components.websocket_api.async_register_command(websocket_list_entities) + hass.components.websocket_api.async_register_command(websocket_get_entity) + hass.components.websocket_api.async_register_command(websocket_update_entity) + hass.components.websocket_api.async_register_command(websocket_remove_entity) return True @async_response +@websocket_api.websocket_command({vol.Required("type"): "config/entity_registry/list"}) async def websocket_list_entities(hass, connection, msg): """Handle list registry entries command. @@ -70,6 +37,12 @@ async def websocket_list_entities(hass, connection, msg): @async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "config/entity_registry/get", + vol.Required("entity_id"): cv.entity_id, + } +) async def websocket_get_entity(hass, connection, msg): """Handle get entity registry entry command. @@ -89,6 +62,17 @@ async def websocket_get_entity(hass, connection, msg): @require_admin @async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "config/entity_registry/update", + vol.Required("entity_id"): cv.entity_id, + # If passed in, we update value. Passing None will remove old value. + vol.Optional("name"): vol.Any(str, None), + vol.Optional("new_entity_id"): str, + # We only allow setting disabled_by user via API. + vol.Optional("disabled_by"): vol.Any("user", None), + } +) async def websocket_update_entity(hass, connection, msg): """Handle update entity websocket command. @@ -107,6 +91,9 @@ async def websocket_update_entity(hass, connection, msg): if "name" in msg: changes["name"] = msg["name"] + if "disabled_by" in msg: + changes["disabled_by"] = msg["disabled_by"] + if "new_entity_id" in msg and msg["new_entity_id"] != msg["entity_id"]: changes["new_entity_id"] = msg["new_entity_id"] if hass.states.get(msg["new_entity_id"]) is not None: @@ -132,6 +119,12 @@ async def websocket_update_entity(hass, connection, msg): @require_admin @async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "config/entity_registry/remove", + vol.Required("entity_id"): cv.entity_id, + } +) async def websocket_remove_entity(hass, connection, msg): """Handle remove entity websocket command. diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 97cc213aa66..8ef41eef9f8 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -201,7 +201,13 @@ class EntityRegistry: @callback def async_update_entity( - self, entity_id, *, name=_UNDEF, new_entity_id=_UNDEF, new_unique_id=_UNDEF + self, + entity_id, + *, + name=_UNDEF, + new_entity_id=_UNDEF, + new_unique_id=_UNDEF, + disabled_by=_UNDEF, ): """Update properties of an entity.""" return self._async_update_entity( @@ -209,6 +215,7 @@ class EntityRegistry: name=name, new_entity_id=new_entity_id, new_unique_id=new_unique_id, + disabled_by=disabled_by, ) @callback @@ -221,20 +228,21 @@ class EntityRegistry: new_entity_id=_UNDEF, device_id=_UNDEF, new_unique_id=_UNDEF, + disabled_by=_UNDEF, ): """Private facing update properties method.""" old = self.entities[entity_id] changes = {} - if name is not _UNDEF and name != old.name: - changes["name"] = name - - if config_entry_id is not _UNDEF and config_entry_id != old.config_entry_id: - changes["config_entry_id"] = config_entry_id - - if device_id is not _UNDEF and device_id != old.device_id: - changes["device_id"] = device_id + for attr_name, value in ( + ("name", name), + ("config_entry_id", config_entry_id), + ("device_id", device_id), + ("disabled_by", disabled_by), + ): + if value is not _UNDEF and value != getattr(old, attr_name): + changes[attr_name] = value if new_entity_id is not _UNDEF and new_entity_id != old.entity_id: if self.async_is_registered(new_entity_id): diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 5f8c6f51acb..f18abe9b0e2 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -105,9 +105,9 @@ async def test_get_entity(hass, client): } -async def test_update_entity_name(hass, client): - """Test updating entity name.""" - mock_registry( +async def test_update_entity(hass, client): + """Test updating entity.""" + registry = mock_registry( hass, { "test_domain.world": RegistryEntry( @@ -133,6 +133,32 @@ async def test_update_entity_name(hass, client): "type": "config/entity_registry/update", "entity_id": "test_domain.world", "name": "after update", + "disabled_by": "user", + } + ) + + msg = await client.receive_json() + + assert msg["result"] == { + "config_entry_id": None, + "device_id": None, + "disabled_by": "user", + "platform": "test_platform", + "entity_id": "test_domain.world", + "name": "after update", + } + + state = hass.states.get("test_domain.world") + assert state.name == "after update" + + assert registry.entities["test_domain.world"].disabled_by == "user" + + await client.send_json( + { + "id": 7, + "type": "config/entity_registry/update", + "entity_id": "test_domain.world", + "disabled_by": None, } ) @@ -147,9 +173,6 @@ async def test_update_entity_name(hass, client): "name": "after update", } - state = hass.states.get("test_domain.world") - assert state.name == "after update" - async def test_update_entity_no_changes(hass, client): """Test update entity with no changes.""" diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 88131a58de0..ce05e914b3d 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -354,6 +354,26 @@ async def test_update_entity_unique_id_conflict(registry): assert mock_schedule_save.call_count == 0 +async def test_update_entity(registry): + """Test updating entity.""" + entry = registry.async_get_or_create( + "light", "hue", "5678", config_entry_id="mock-id-1" + ) + + for attr_name, new_value in ( + ("name", "new name"), + ("disabled_by", entity_registry.DISABLED_USER), + ): + changes = {attr_name: new_value} + updated_entry = registry.async_update_entity(entry.entity_id, **changes) + + assert updated_entry != entry + assert getattr(updated_entry, attr_name) == new_value + assert getattr(updated_entry, attr_name) != getattr(entry, attr_name) + + entry = updated_entry + + async def test_disabled_by(registry): """Test that we can disable an entry when we create it.""" entry = registry.async_get_or_create("light", "hue", "5678", disabled_by="hass") From c55241960ca056918841730a745d803d62f7f0af Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 17 Aug 2019 10:40:51 +0200 Subject: [PATCH 175/273] Upgrade voluptuous-serialize to 2.2.0 (#26008) --- 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 165cea59a03..a898dc497d8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.99 sqlalchemy==1.3.7 -voluptuous-serialize==2.1.0 +voluptuous-serialize==2.2.0 voluptuous==0.11.7 zeroconf==0.23.0 diff --git a/requirements_all.txt b/requirements_all.txt index f6bfb80d480..397c7ebb599 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -17,7 +17,7 @@ pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.99 voluptuous==0.11.7 -voluptuous-serialize==2.1.0 +voluptuous-serialize==2.2.0 # homeassistant.components.nuimo_controller --only-binary=all nuimo==0.1.0 diff --git a/setup.py b/setup.py index 148d8a7992f..01288c01bf1 100755 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ REQUIRES = [ "requests==2.22.0", "ruamel.yaml==0.15.99", "voluptuous==0.11.7", - "voluptuous-serialize==2.1.0", + "voluptuous-serialize==2.2.0", ] MIN_PY_VERSION = ".".join(map(str, hass_const.REQUIRED_PYTHON_VER)) From ca74a23cf191dc6ca081d1496c9d0afbbfd65b04 Mon Sep 17 00:00:00 2001 From: Oncleben31 Date: Sat, 17 Aug 2019 11:47:28 +0200 Subject: [PATCH 176/273] Fix unnecessary db entries for metro_france (#25957) * fix #25911 avoid db unecessary entries * use UTC timestamp for forecast * simplify forecast timestamp --- homeassistant/components/meteo_france/__init__.py | 2 +- homeassistant/components/meteo_france/weather.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 89fcc3c98aa..d6460fd6e5a 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -121,7 +121,7 @@ def setup(hass, config): weather_alert_client.update_data() except VigilanceMeteoError as exp: _LOGGER.error( - "Unexpected error when creating the" "vigilance_meteoFrance proxy: %s ", + "Unexpected error when creating the vigilance_meteoFrance proxy: %s ", exp, ) else: diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index ce21e0f7d77..9a861d13c2e 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -1,5 +1,5 @@ """Support for Meteo-France weather service.""" -from datetime import datetime, timedelta +from datetime import timedelta import logging from homeassistant.components.weather import ( @@ -9,6 +9,7 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TIME, WeatherEntity, ) +import homeassistant.util.dt as dt_util from homeassistant.const import TEMP_CELSIUS from . import ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DATA_METEO_FRANCE @@ -83,8 +84,9 @@ class MeteoFranceWeather(WeatherEntity): @property def forecast(self): """Return the forecast.""" - reftime = datetime.now().replace(hour=12, minute=00) + reftime = dt_util.utcnow().replace(hour=12, minute=0, second=0, microsecond=0) reftime += timedelta(hours=24) + _LOGGER.debug("reftime used for %s forecast: %s", self._data["name"], reftime) forecast_data = [] for key in self._data["forecast"]: value = self._data["forecast"][key] From 9dff2e188b43ade7f7677ae5767e9e962073b051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 17 Aug 2019 21:50:41 +0300 Subject: [PATCH 177/273] Tone down huawei_lte logging (#26020) * Tone down huawei_lte logging Refs https://github.com/home-assistant/home-assistant/pull/23809 * Fix import order --- homeassistant/components/huawei_lte/__init__.py | 4 ++-- .../components/huawei_lte/device_tracker.py | 17 +++++++++++------ homeassistant/components/huawei_lte/sensor.py | 2 +- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 51d0dc5d3a2..2cbc271219b 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -101,8 +101,8 @@ class RouterData: if debugging or path in self._subscriptions: try: setattr(self, path, func()) - except ResponseErrorNotSupportedException as ex: - _LOGGER.warning("%s not supported by device", path, exc_info=ex) + except ResponseErrorNotSupportedException: + _LOGGER.warning("%s not supported by device", path) self._subscriptions.discard(path) finally: _LOGGER.debug("%s=%s", path, getattr(self, path)) diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 878a819aaae..697b2a3ed3c 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -1,4 +1,5 @@ """Support for device tracking of Huawei LTE routers.""" +import logging from typing import Any, Dict, List, Optional import attr @@ -9,9 +10,12 @@ from homeassistant.components.device_tracker import PLATFORM_SCHEMA, DeviceScann from homeassistant.const import CONF_URL from . import DATA_KEY, RouterData + +_LOGGER = logging.getLogger(__name__) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_URL): cv.url}) -HOSTS_PATH = "wlan_host_list.Hosts" +HOSTS_PATH = "wlan_host_list.Hosts.Host" def get_scanner(hass, config): @@ -32,11 +36,12 @@ class HuaweiLteScanner(DeviceScanner): def scan_devices(self) -> List[str]: """Scan for devices.""" self.data.update() - self._hosts = { - x["MacAddress"]: x - for x in self.data[HOSTS_PATH + ".Host"] - if x.get("MacAddress") - } + try: + self._hosts = { + x["MacAddress"]: x for x in self.data[HOSTS_PATH] if x.get("MacAddress") + } + except KeyError: + _LOGGER.debug("%s not in data", HOSTS_PATH) return list(self._hosts) def get_device_name(self, device: str) -> Optional[str]: diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index e72bd3aa438..4ef88eb783e 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -203,7 +203,7 @@ class HuaweiLteSensor(Entity): try: value = self.data[self.path] except KeyError: - _LOGGER.warning("%s not in data", self.path) + _LOGGER.debug("%s not in data", self.path) value = None formatter = self.meta.get("formatter") From 6bf35232b92fed39e7d0e7ac7255a3458458bc7f Mon Sep 17 00:00:00 2001 From: Dima Zavin Date: Sat, 17 Aug 2019 14:50:15 -0700 Subject: [PATCH 178/273] Don't force a query to the main lutron repeater on update (#25939) We only want to force a query if we don't have any previous state. Otherwise, we should be tracking the state via continuous status updates. For lights (not switches) the extra query was also superfluous since it was already querying on startup. We should probably have a timeout on that so at some point we'll requery in case remote end disconnected/rebooted, etc. Leaving for another PR. --- homeassistant/components/lutron/light.py | 2 -- homeassistant/components/lutron/switch.py | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lutron/light.py b/homeassistant/components/lutron/light.py index 138d4882446..cae2fc5cfdd 100644 --- a/homeassistant/components/lutron/light.py +++ b/homeassistant/components/lutron/light.py @@ -34,7 +34,6 @@ class LutronLight(LutronDevice, Light): def __init__(self, area_name, lutron_device, controller): """Initialize the light.""" self._prev_brightness = None - self._is_on = False super().__init__(area_name, lutron_device, controller) @property @@ -78,6 +77,5 @@ class LutronLight(LutronDevice, Light): def update(self): """Call when forcing a refresh of the device.""" - self._is_on = self._lutron_device.level > 0 if self._prev_brightness is None: self._prev_brightness = to_hass_level(self._lutron_device.level) diff --git a/homeassistant/components/lutron/switch.py b/homeassistant/components/lutron/switch.py index 7aa5334cbf2..604f19fc2ae 100644 --- a/homeassistant/components/lutron/switch.py +++ b/homeassistant/components/lutron/switch.py @@ -23,7 +23,7 @@ class LutronSwitch(LutronDevice, SwitchDevice): def __init__(self, area_name, lutron_device, controller): """Initialize the switch.""" - self._is_on = False + self._prev_state = None super().__init__(area_name, lutron_device, controller) def turn_on(self, **kwargs): @@ -48,4 +48,5 @@ class LutronSwitch(LutronDevice, SwitchDevice): def update(self): """Call when forcing a refresh of the device.""" - self._is_on = self._lutron_device.level > 0 + if self._prev_state is None: + self._prev_state = self._lutron_device.level > 0 From fed789c770d033521658b5afac37712c64c7ceec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 18 Aug 2019 01:47:55 +0300 Subject: [PATCH 179/273] Upgrade pre-commit to 1.18.2 (#26029) https://github.com/pre-commit/pre-commit/blob/v1.18.2/CHANGELOG.md --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index bcbc53f2649..77162b55d7b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,6 +11,7 @@ flake8-docstrings==1.3.1 flake8==3.7.8 mock-open==1.3.1 mypy==0.720 +pre-commit==1.18.2 pydocstyle==4.0.0 pylint==2.3.1 pytest-aiohttp==0.3.0 @@ -19,4 +20,3 @@ pytest-sugar==0.9.2 pytest-timeout==1.3.3 pytest==5.0.1 requests_mock==1.6.0 -pre-commit==1.17.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b9ce85b1297..fcdf314727e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -12,6 +12,7 @@ flake8-docstrings==1.3.1 flake8==3.7.8 mock-open==1.3.1 mypy==0.720 +pre-commit==1.18.2 pydocstyle==4.0.0 pylint==2.3.1 pytest-aiohttp==0.3.0 @@ -20,7 +21,6 @@ pytest-sugar==0.9.2 pytest-timeout==1.3.3 pytest==5.0.1 requests_mock==1.6.0 -pre-commit==1.17.0 # homeassistant.components.homekit From 88ff25b5096880850ee71cc553900fd790ac9919 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 18 Aug 2019 04:28:46 +0200 Subject: [PATCH 180/273] Update hole to 0.5.0 (#26022) --- homeassistant/components/pi_hole/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/pi_hole/manifest.json b/homeassistant/components/pi_hole/manifest.json index c47d8811e68..2d19ab25fe7 100644 --- a/homeassistant/components/pi_hole/manifest.json +++ b/homeassistant/components/pi_hole/manifest.json @@ -3,7 +3,7 @@ "name": "Pi hole", "documentation": "https://www.home-assistant.io/components/pi_hole", "requirements": [ - "hole==0.3.0" + "hole==0.5.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 397c7ebb599..24205f953d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -621,7 +621,7 @@ hkavr==0.0.5 hlk-sw16==0.0.7 # homeassistant.components.pi_hole -hole==0.3.0 +hole==0.5.0 # homeassistant.components.workday holidays==0.9.11 From 8da18795993acecf723486fbad3853c3a24b9871 Mon Sep 17 00:00:00 2001 From: Jonathan Keljo Date: Sat, 17 Aug 2019 19:32:01 -0700 Subject: [PATCH 181/273] Fix background crash in sisyphus integration (#26032) I think I was thinking in another language, since this was not valid Python. Result was that the thread that maintained the SocketIO connection to the table would die early on, so no status updates were actually flowing from the table. --- homeassistant/components/sisyphus/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sisyphus/__init__.py b/homeassistant/components/sisyphus/__init__.py index 09fcda7f8c8..771641c9b1d 100644 --- a/homeassistant/components/sisyphus/__init__.py +++ b/homeassistant/components/sisyphus/__init__.py @@ -35,7 +35,7 @@ async def async_setup(hass, config): """Filters out excessively verbose logs from SocketIO.""" def filter(self, record): - if record.msg.contains("waiting for connection"): + if "waiting for connection" in record.msg: return False return True From 2bd832cd7afa7e48bf4d0fce4d46cef3addbbda1 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sun, 18 Aug 2019 05:14:46 +0100 Subject: [PATCH 182/273] Skip homekit_controller polls when system is overloaded and still trying to process the previous one (#25968) * Skip async_update if there are signs of backpressure * Black * Only warn once * Log on recovery * Formatting fix --- .../homekit_controller/connection.py | 49 +++++++++++++------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 8068f26aaa3..1cb2131fb8f 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -103,6 +103,10 @@ class HKDevice: # this method. self._polling_interval_remover = None + # Never allow concurrent polling of the same accessory or bridge + self._polling_lock = asyncio.Lock() + self._polling_lock_warned = False + def add_pollable_characteristics(self, characteristics): """Add (aid, iid) pairs that we need to poll.""" self.pollable_characteristics.extend(characteristics) @@ -247,25 +251,40 @@ class HKDevice: _LOGGER.debug("HomeKit connection not polling any characteristics.") return - _LOGGER.debug("Starting HomeKit controller update") + if self._polling_lock.locked(): + if not self._polling_lock_warned: + _LOGGER.warning( + "HomeKit controller update skipped as previous poll still in flight" + ) + self._polling_lock_warned = True + return - try: - new_values_dict = await self.get_characteristics( - self.pollable_characteristics + if self._polling_lock_warned: + _LOGGER.info( + "HomeKit controller no longer detecting back pressure - not skipping poll" ) - except AccessoryNotFoundError: - # Not only did the connection fail, but also the accessory is not - # visible on the network. - self.async_set_unavailable() - return - except (AccessoryDisconnectedError, EncryptionError): - # Temporary connection failure. Device is still available but our - # connection was dropped. - return + self._polling_lock_warned = False - self.process_new_events(new_values_dict) + async with self._polling_lock: + _LOGGER.debug("Starting HomeKit controller update") - _LOGGER.debug("Finished HomeKit controller update") + try: + new_values_dict = await self.get_characteristics( + self.pollable_characteristics + ) + except AccessoryNotFoundError: + # Not only did the connection fail, but also the accessory is not + # visible on the network. + self.async_set_unavailable() + return + except (AccessoryDisconnectedError, EncryptionError): + # Temporary connection failure. Device is still available but our + # connection was dropped. + return + + self.process_new_events(new_values_dict) + + _LOGGER.debug("Finished HomeKit controller update") def process_new_events(self, new_values_dict): """Process events from accessory into HA state.""" From 4bcee4f7ceb289fb6ab486fc1f167fa5f0091805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 18 Aug 2019 07:15:55 +0300 Subject: [PATCH 183/273] Run flake8 on pre-commit (#26028) --- .pre-commit-config.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1b2cb9d69b1..78b7ec29859 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,3 +6,10 @@ repos: args: - --safe - --quiet +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.7.8 + hooks: + - id: flake8 + additional_dependencies: + - flake8-docstrings==1.3.1 + - pydocstyle==4.0.0 From 65aa4148a4656edec97e64c5dccde42f1c1ad546 Mon Sep 17 00:00:00 2001 From: Jonathan Keljo Date: Sat, 17 Aug 2019 21:16:13 -0700 Subject: [PATCH 184/273] Upgrade sisyphus-control to 2.2.1 (#26033) This picks up a bugfix for a crash on shutdown, so that the `sisyphus` component can now shut down cleanly. --- homeassistant/components/sisyphus/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sisyphus/manifest.json b/homeassistant/components/sisyphus/manifest.json index cac821aa9f2..f8e6e1bf14d 100644 --- a/homeassistant/components/sisyphus/manifest.json +++ b/homeassistant/components/sisyphus/manifest.json @@ -3,7 +3,7 @@ "name": "Sisyphus", "documentation": "https://www.home-assistant.io/components/sisyphus", "requirements": [ - "sisyphus-control==2.2" + "sisyphus-control==2.2.1" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 24205f953d8..439064864c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1699,7 +1699,7 @@ simplepush==1.1.4 simplisafe-python==4.3.0 # homeassistant.components.sisyphus -sisyphus-control==2.2 +sisyphus-control==2.2.1 # homeassistant.components.skybell skybellpy==0.4.0 From 6907e8e9dcf668d78246793672e040dd839419a9 Mon Sep 17 00:00:00 2001 From: Jay Date: Sun, 18 Aug 2019 00:19:13 -0400 Subject: [PATCH 185/273] Upgrade Dialogflow to work with V2 API (#25975) * Handle v1 and v2 dialog api requests * Stylistic changes --- .../components/dialogflow/__init__.py | 46 +- tests/components/dialogflow/test_init.py | 464 +++++++++--------- 2 files changed, 283 insertions(+), 227 deletions(-) diff --git a/homeassistant/components/dialogflow/__init__.py b/homeassistant/components/dialogflow/__init__.py index 4f2876df296..45fee0f867e 100644 --- a/homeassistant/components/dialogflow/__init__.py +++ b/homeassistant/components/dialogflow/__init__.py @@ -17,6 +17,9 @@ SOURCE = "Home Assistant Dialogflow" CONFIG_SCHEMA = vol.Schema({DOMAIN: {}}, extra=vol.ALLOW_EXTRA) +V1 = 1 +V2 = 2 + class DialogFlowError(HomeAssistantError): """Raised when a DialogFlow error happens.""" @@ -84,23 +87,45 @@ async_remove_entry = config_entry_flow.webhook_async_remove_entry def dialogflow_error_response(message, error): """Return a response saying the error message.""" - dialogflow_response = DialogflowResponse(message["result"]["parameters"]) + api_version = get_api_version(message) + if api_version is V1: + parameters = message["result"]["parameters"] + elif api_version is V2: + parameters = message["queryResult"]["parameters"] + dialogflow_response = DialogflowResponse(parameters, api_version) dialogflow_response.add_speech(error) return dialogflow_response.as_dict() +def get_api_version(message): + """Get API version of Dialogflow message.""" + if message.get("id") is not None: + return V1 + if message.get("responseId") is not None: + return V2 + + async def async_handle_message(hass, message): """Handle a DialogFlow message.""" - req = message.get("result") - action_incomplete = req["actionIncomplete"] + _api_version = get_api_version(message) + if _api_version is V1: + _LOGGER.warning( + "Dialogflow V1 API will be removed on October 23, 2019. Please change your DialogFlow settings to use the V2 api" + ) + req = message.get("result") + action_incomplete = req.get("actionIncomplete", True) + if action_incomplete: + return - if action_incomplete: - return None + elif _api_version is V2: + req = message.get("queryResult") + if req.get("allRequiredParamsPresent", False) is False: + return action = req.get("action", "") parameters = req.get("parameters").copy() parameters["dialogflow_query"] = message - dialogflow_response = DialogflowResponse(parameters) + dialogflow_response = DialogflowResponse(parameters, _api_version) if action == "": raise DialogFlowError( @@ -123,10 +148,11 @@ async def async_handle_message(hass, message): class DialogflowResponse: """Help generating the response for Dialogflow.""" - def __init__(self, parameters): + def __init__(self, parameters, api_version): """Initialize the Dialogflow response.""" self.speech = None self.parameters = {} + self.api_version = api_version # Parameter names replace '.' and '-' for '_' for key, value in parameters.items(): underscored_key = key.replace(".", "_").replace("-", "_") @@ -143,4 +169,8 @@ class DialogflowResponse: def as_dict(self): """Return response in a Dialogflow valid dictionary.""" - return {"speech": self.speech, "displayText": self.speech, "source": SOURCE} + if self.api_version is V1: + return {"speech": self.speech, "displayText": self.speech, "source": SOURCE} + + if self.api_version is V2: + return {"fulfillmentText": self.speech, "source": SOURCE} diff --git a/tests/components/dialogflow/test_init.py b/tests/components/dialogflow/test_init.py index 25e563ebf9b..18a03ff2603 100644 --- a/tests/components/dialogflow/test_init.py +++ b/tests/components/dialogflow/test_init.py @@ -1,5 +1,6 @@ """The tests for the Dialogflow component.""" import json +import copy from unittest.mock import Mock import pytest @@ -90,108 +91,16 @@ async def fixture(hass, aiohttp_client): return await aiohttp_client(hass.http.app), webhook_id -async def test_intent_action_incomplete(fixture): - """Test when action is not completed.""" - mock_client, webhook_id = fixture - data = { +class _Data: + _v1 = { "id": REQUEST_ID, "timestamp": REQUEST_TIMESTAMP, "result": { "source": "agent", "resolvedQuery": "my zodiac sign is virgo", - "speech": "", - "action": "GetZodiacHoroscopeIntent", - "actionIncomplete": True, - "parameters": {"ZodiacSign": "virgo"}, - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "false", - "intentName": INTENT_NAME, - }, - "fulfillment": {"speech": "", "messages": [{"type": 0, "speech": ""}]}, - "score": 1, - }, - "status": {"code": 200, "errorType": "success"}, - "sessionId": SESSION_ID, - "originalRequest": None, - } - - response = await mock_client.post( - "/api/webhook/{}".format(webhook_id), data=json.dumps(data) - ) - assert 200 == response.status - assert "" == await response.text() - - -async def test_intent_slot_filling(fixture): - """Test when Dialogflow asks for slot-filling return none.""" - mock_client, webhook_id = fixture - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "my zodiac sign is", - "speech": "", - "action": "GetZodiacHoroscopeIntent", - "actionIncomplete": True, - "parameters": {"ZodiacSign": ""}, - "contexts": [ - { - "name": CONTEXT_NAME, - "parameters": {"ZodiacSign.original": "", "ZodiacSign": ""}, - "lifespan": 2, - }, - { - "name": "tests_ha_dialog_context", - "parameters": {"ZodiacSign.original": "", "ZodiacSign": ""}, - "lifespan": 2, - }, - { - "name": "tests_ha_dialog_params_zodiacsign", - "parameters": {"ZodiacSign.original": "", "ZodiacSign": ""}, - "lifespan": 1, - }, - ], - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "true", - "intentName": INTENT_NAME, - }, - "fulfillment": { - "speech": "What is the ZodiacSign?", - "messages": [{"type": 0, "speech": "What is the ZodiacSign?"}], - }, - "score": 0.77, - }, - "status": {"code": 200, "errorType": "success"}, - "sessionId": SESSION_ID, - "originalRequest": None, - } - - response = await mock_client.post( - "/api/webhook/{}".format(webhook_id), data=json.dumps(data) - ) - assert 200 == response.status - assert "" == await response.text() - - -async def test_intent_request_with_parameters(fixture): - """Test a request with parameters.""" - mock_client, webhook_id = fixture - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "my zodiac sign is virgo", - "speech": "", "action": "GetZodiacHoroscopeIntent", "actionIncomplete": False, "parameters": {"ZodiacSign": "virgo"}, - "contexts": [], "metadata": { "intentId": INTENT_ID, "webhookUsed": "true", @@ -205,6 +114,122 @@ async def test_intent_request_with_parameters(fixture): "sessionId": SESSION_ID, "originalRequest": None, } + + _v2 = { + "responseId": REQUEST_ID, + "timestamp": REQUEST_TIMESTAMP, + "queryResult": { + "queryText": "my zodiac sign is virgo", + "action": "GetZodiacHoroscopeIntent", + "allRequiredParamsPresent": True, + "parameters": {"ZodiacSign": "virgo"}, + "intent": { + "name": INTENT_ID, + "webhookState": "true", + "displayName": INTENT_NAME, + }, + "fulfillment": {"text": "", "messages": [{"type": 0, "speech": ""}]}, + "intentDetectionConfidence": 1, + }, + "status": {"code": 200, "errorType": "success"}, + "session": SESSION_ID, + "originalDetectIntentRequest": None, + } + + @property + def v1(self): + return copy.deepcopy(self._v1) + + @property + def v2(self): + return copy.deepcopy(self._v2) + + +Data = _Data() + + +async def test_v1_data(): + """Test for version 1 api based on message.""" + assert dialogflow.get_api_version(Data.v1) == 1 + + +async def test_v2_data(): + """Test for version 2 api based on message.""" + assert dialogflow.get_api_version(Data.v2) == 2 + + +async def test_intent_action_incomplete_v1(fixture): + """Test when action is not completed.""" + mock_client, webhook_id = fixture + data = Data.v1 + data["result"]["actionIncomplete"] = True + + response = await mock_client.post( + "/api/webhook/{}".format(webhook_id), data=json.dumps(data) + ) + assert 200 == response.status + assert "" == await response.text() + + +async def test_intent_action_incomplete_v2(fixture): + """Test when action is not completed.""" + mock_client, webhook_id = fixture + data = Data.v2 + data["queryResult"]["allRequiredParamsPresent"] = False + + response = await mock_client.post( + "/api/webhook/{}".format(webhook_id), data=json.dumps(data) + ) + assert 200 == response.status + assert "" == await response.text() + + +async def test_intent_slot_filling_v1(fixture): + """Test when Dialogflow asks for slot-filling return none.""" + mock_client, webhook_id = fixture + + data = Data.v1 + data["result"].update( + resolvedQuery="my zodiac sign is", + speech="", + actionIncomplete=True, + parameters={"ZodiacSign": ""}, + contexts=[ + { + "name": CONTEXT_NAME, + "parameters": {"ZodiacSign.original": "", "ZodiacSign": ""}, + "lifespan": 2, + }, + { + "name": "tests_ha_dialog_context", + "parameters": {"ZodiacSign.original": "", "ZodiacSign": ""}, + "lifespan": 2, + }, + { + "name": "tests_ha_dialog_params_zodiacsign", + "parameters": {"ZodiacSign.original": "", "ZodiacSign": ""}, + "lifespan": 1, + }, + ], + fulfillment={ + "speech": "What is the ZodiacSign?", + "messages": [{"type": 0, "speech": "What is the ZodiacSign?"}], + }, + score=0.77, + ) + data["result"]["metadata"].update(webhookForSlotFillingUsed="true") + + response = await mock_client.post( + "/api/webhook/{}".format(webhook_id), data=json.dumps(data) + ) + assert 200 == response.status + assert "" == await response.text() + + +async def test_intent_request_with_parameters_v1(fixture): + """Test a request with parameters.""" + mock_client, webhook_id = fixture + data = Data.v1 response = await mock_client.post( "/api/webhook/{}".format(webhook_id), data=json.dumps(data) ) @@ -213,33 +238,23 @@ async def test_intent_request_with_parameters(fixture): assert "You told us your sign is virgo." == text -async def test_intent_request_with_parameters_but_empty(fixture): +async def test_intent_request_with_parameters_v2(fixture): + """Test a request with parameters.""" + mock_client, webhook_id = fixture + data = Data.v2 + response = await mock_client.post( + "/api/webhook/{}".format(webhook_id), data=json.dumps(data) + ) + assert 200 == response.status + text = (await response.json()).get("fulfillmentText") + assert "You told us your sign is virgo." == text + + +async def test_intent_request_with_parameters_but_empty_v1(fixture): """Test a request with parameters but empty value.""" mock_client, webhook_id = fixture - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "my zodiac sign is virgo", - "speech": "", - "action": "GetZodiacHoroscopeIntent", - "actionIncomplete": False, - "parameters": {"ZodiacSign": ""}, - "contexts": [], - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "false", - "intentName": INTENT_NAME, - }, - "fulfillment": {"speech": "", "messages": [{"type": 0, "speech": ""}]}, - "score": 1, - }, - "status": {"code": 200, "errorType": "success"}, - "sessionId": SESSION_ID, - "originalRequest": None, - } + data = Data.v1 + data["result"].update(parameters={"ZodiacSign": ""}) response = await mock_client.post( "/api/webhook/{}".format(webhook_id), data=json.dumps(data) ) @@ -248,33 +263,30 @@ async def test_intent_request_with_parameters_but_empty(fixture): assert "You told us your sign is ." == text -async def test_intent_request_without_slots(hass, fixture): +async def test_intent_request_with_parameters_but_empty_v2(fixture): + """Test a request with parameters but empty value.""" + mock_client, webhook_id = fixture + data = Data.v2 + data["queryResult"].update(parameters={"ZodiacSign": ""}) + response = await mock_client.post( + "/api/webhook/{}".format(webhook_id), data=json.dumps(data) + ) + assert 200 == response.status + text = (await response.json()).get("fulfillmentText") + assert "You told us your sign is ." == text + + +async def test_intent_request_without_slots_v1(hass, fixture): """Test a request without slots.""" mock_client, webhook_id = fixture - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "where are we", - "speech": "", - "action": "WhereAreWeIntent", - "actionIncomplete": False, - "parameters": {}, - "contexts": [], - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "false", - "intentName": INTENT_NAME, - }, - "fulfillment": {"speech": "", "messages": [{"type": 0, "speech": ""}]}, - "score": 1, - }, - "status": {"code": 200, "errorType": "success"}, - "sessionId": SESSION_ID, - "originalRequest": None, - } + data = Data.v1 + data["result"].update( + resolvedQuery="where are we", + action="WhereAreWeIntent", + parameters={}, + contexts=[], + ) + response = await mock_client.post( "/api/webhook/{}".format(webhook_id), data=json.dumps(data) ) @@ -294,37 +306,45 @@ async def test_intent_request_without_slots(hass, fixture): assert "You are both home, you silly" == text -async def test_intent_request_calling_service(fixture, calls): +async def test_intent_request_without_slots_v2(hass, fixture): + """Test a request without slots.""" + mock_client, webhook_id = fixture + data = Data.v2 + data["queryResult"].update( + queryText="where are we", + action="WhereAreWeIntent", + parameters={}, + outputContexts=[], + ) + + response = await mock_client.post( + "/api/webhook/{}".format(webhook_id), data=json.dumps(data) + ) + assert 200 == response.status + text = (await response.json()).get("fulfillmentText") + + assert "Anne Therese is at unknown and Paulus is at unknown" == text + + hass.states.async_set("device_tracker.paulus", "home") + hass.states.async_set("device_tracker.anne_therese", "home") + + response = await mock_client.post( + "/api/webhook/{}".format(webhook_id), data=json.dumps(data) + ) + assert 200 == response.status + text = (await response.json()).get("fulfillmentText") + assert "You are both home, you silly" == text + + +async def test_intent_request_calling_service_v1(fixture, calls): """Test a request for calling a service. If this request is done async the test could finish before the action has been executed. Hard to test because it will be a race condition. """ mock_client, webhook_id = fixture - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "my zodiac sign is virgo", - "speech": "", - "action": "CallServiceIntent", - "actionIncomplete": False, - "parameters": {"ZodiacSign": "virgo"}, - "contexts": [], - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "false", - "intentName": INTENT_NAME, - }, - "fulfillment": {"speech": "", "messages": [{"type": 0, "speech": ""}]}, - "score": 1, - }, - "status": {"code": 200, "errorType": "success"}, - "sessionId": SESSION_ID, - "originalRequest": None, - } + data = Data.v1 + data["result"]["action"] = "CallServiceIntent" call_count = len(calls) response = await mock_client.post( "/api/webhook/{}".format(webhook_id), data=json.dumps(data) @@ -338,33 +358,34 @@ async def test_intent_request_calling_service(fixture, calls): assert "virgo" == call.data.get("hello") -async def test_intent_with_no_action(fixture): +async def test_intent_request_calling_service_v2(fixture, calls): + """Test a request for calling a service. + + If this request is done async the test could finish before the action + has been executed. Hard to test because it will be a race condition. + """ + mock_client, webhook_id = fixture + data = Data.v2 + data["queryResult"]["action"] = "CallServiceIntent" + call_count = len(calls) + response = await mock_client.post( + "/api/webhook/{}".format(webhook_id), data=json.dumps(data) + ) + assert 200 == response.status + assert call_count + 1 == len(calls) + call = calls[-1] + assert "test" == call.domain + assert "dialogflow" == call.service + assert ["switch.test"] == call.data.get("entity_id") + assert "virgo" == call.data.get("hello") + + +async def test_intent_with_no_action_v1(fixture): """Test an intent with no defined action.""" mock_client, webhook_id = fixture - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "my zodiac sign is virgo", - "speech": "", - "action": "", - "actionIncomplete": False, - "parameters": {"ZodiacSign": ""}, - "contexts": [], - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "false", - "intentName": INTENT_NAME, - }, - "fulfillment": {"speech": "", "messages": [{"type": 0, "speech": ""}]}, - "score": 1, - }, - "status": {"code": 200, "errorType": "success"}, - "sessionId": SESSION_ID, - "originalRequest": None, - } + data = Data.v1 + del data["result"]["action"] + assert "action" not in data["result"] response = await mock_client.post( "/api/webhook/{}".format(webhook_id), data=json.dumps(data) ) @@ -373,36 +394,41 @@ async def test_intent_with_no_action(fixture): assert "You have not defined an action in your Dialogflow intent." == text -async def test_intent_with_unknown_action(fixture): +async def test_intent_with_no_action_v2(fixture): + """Test an intent with no defined action.""" + mock_client, webhook_id = fixture + data = Data.v2 + del data["queryResult"]["action"] + assert "action" not in data["queryResult"] + response = await mock_client.post( + "/api/webhook/{}".format(webhook_id), data=json.dumps(data) + ) + assert 200 == response.status + text = (await response.json()).get("fulfillmentText") + assert "You have not defined an action in your Dialogflow intent." == text + + +async def test_intent_with_unknown_action_v1(fixture): """Test an intent with an action not defined in the conf.""" mock_client, webhook_id = fixture - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "my zodiac sign is virgo", - "speech": "", - "action": "unknown", - "actionIncomplete": False, - "parameters": {"ZodiacSign": ""}, - "contexts": [], - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "false", - "intentName": INTENT_NAME, - }, - "fulfillment": {"speech": "", "messages": [{"type": 0, "speech": ""}]}, - "score": 1, - }, - "status": {"code": 200, "errorType": "success"}, - "sessionId": SESSION_ID, - "originalRequest": None, - } + data = Data.v1 + data["result"]["action"] = "unknown" response = await mock_client.post( "/api/webhook/{}".format(webhook_id), data=json.dumps(data) ) assert 200 == response.status text = (await response.json()).get("speech") assert "This intent is not yet configured within Home Assistant." == text + + +async def test_intent_with_unknown_action_v2(fixture): + """Test an intent with an action not defined in the conf.""" + mock_client, webhook_id = fixture + data = Data.v2 + data["queryResult"]["action"] = "unknown" + response = await mock_client.post( + "/api/webhook/{}".format(webhook_id), data=json.dumps(data) + ) + assert 200 == response.status + text = (await response.json()).get("fulfillmentText") + assert "This intent is not yet configured within Home Assistant." == text From fc716a45c91e340f9afbedd5b7a72eaca3fcee7b Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Sun, 18 Aug 2019 00:19:44 -0400 Subject: [PATCH 186/273] Updates to Environment Canada components (#25973) * Bump env_canada to 0.0.21 * Add timestamp attribute to camera * Bump env_canada to 0.0.23 * Clean up displayed values * Validate radar station code * Bump env_canada to 0.0.24 * Black * Remove default "None" from sensor * Switch to cv.matches_regex --- homeassistant/components/environment_canada/camera.py | 6 +++++- homeassistant/components/environment_canada/manifest.json | 2 +- homeassistant/components/environment_canada/sensor.py | 7 ++++++- requirements_all.txt | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index 709e4251fbf..2a23fb95a18 100755 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -23,6 +23,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_STATION = "station" ATTR_LOCATION = "location" +ATTR_UPDATED = "updated" CONF_ATTRIBUTION = "Data provided by Environment Canada" CONF_STATION = "station" @@ -35,7 +36,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_LOOP, default=True): cv.boolean, vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_STATION): cv.string, + vol.Optional(CONF_STATION): cv.matches_regex(r"^C[A-Z]{4}$|^[A-Z]{3}$"), vol.Inclusive(CONF_LATITUDE, "latlon"): cv.latitude, vol.Inclusive(CONF_LONGITUDE, "latlon"): cv.longitude, vol.Optional(CONF_PRECIP_TYPE): ["RAIN", "SNOW"], @@ -70,6 +71,7 @@ class ECCamera(Camera): self.camera_name = camera_name self.content_type = "image/gif" self.image = None + self.timestamp = None def camera_image(self): """Return bytes of camera image.""" @@ -90,6 +92,7 @@ class ECCamera(Camera): ATTR_ATTRIBUTION: CONF_ATTRIBUTION, ATTR_LOCATION: self.radar_object.station_name, ATTR_STATION: self.radar_object.station_code, + ATTR_UPDATED: self.timestamp, } return attr @@ -101,3 +104,4 @@ class ECCamera(Camera): self.image = self.radar_object.get_loop() else: self.image = self.radar_object.get_latest_frame() + self.timestamp = self.radar_object.timestamp.isoformat() diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 85df0495428..0625fd4c27f 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -3,7 +3,7 @@ "name": "Environment Canada", "documentation": "https://www.home-assistant.io/components/environment_canada", "requirements": [ - "env_canada==0.0.20" + "env_canada==0.0.24" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 0182e7c67ed..2413edaebce 100755 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -133,10 +133,15 @@ class ECSensor(Entity): ATTR_TIME: " | ".join([str(s.get("date")) for s in value]), } ) + elif self.sensor_type == "tendency": + self._state = str(value).capitalize() else: self._state = value - if sensor_data.get("unit") == "C": + if sensor_data.get("unit") == "C" or self.sensor_type in [ + "wind_chill", + "humidex", + ]: self._unit = TEMP_CELSIUS else: self._unit = sensor_data.get("unit") diff --git a/requirements_all.txt b/requirements_all.txt index 439064864c7..b652cf81017 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -437,7 +437,7 @@ enocean==0.50 enturclient==0.2.0 # homeassistant.components.environment_canada -env_canada==0.0.20 +env_canada==0.0.24 # homeassistant.components.envirophat # envirophat==0.0.6 From a2589f56e1ac87023123dfb98dffc968073c6ca5 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 18 Aug 2019 06:34:11 +0200 Subject: [PATCH 187/273] Add system options to config entries (#25926) * Add system options to config entries * For feedback * Follow most of balloobs comments * Fix balloobs comments * Improvements * Fix second round of Balloobs comments * Fix third round * Add system options to mock config entry * Fix integration tests * Fix the last failing tests * Fix disabled string * Fix failing disabled_by tests * New tests * Config entry WS API tests * Fix comments --- .../components/config/config_entries.py | 45 ++++++++++++++++ homeassistant/components/ps4/__init__.py | 2 +- homeassistant/config_entries.py | 27 +++++++++- homeassistant/helpers/entity_platform.py | 2 +- homeassistant/helpers/entity_registry.py | 23 +++++++- tests/common.py | 2 + tests/components/axis/test_binary_sensor.py | 1 + tests/components/axis/test_camera.py | 1 + tests/components/axis/test_switch.py | 1 + .../components/config/test_config_entries.py | 44 +++++++++++++++ tests/components/deconz/test_binary_sensor.py | 3 +- tests/components/deconz/test_climate.py | 3 +- tests/components/deconz/test_cover.py | 1 + tests/components/deconz/test_light.py | 3 +- tests/components/deconz/test_scene.py | 1 + tests/components/deconz/test_sensor.py | 3 +- tests/components/deconz/test_switch.py | 1 + .../homekit_controller/test_storage.py | 1 + tests/components/hue/test_light.py | 1 + tests/components/hue/test_sensor_base.py | 1 + tests/components/ps4/test_init.py | 2 +- tests/components/ps4/test_media_player.py | 2 +- tests/components/smartthings/conftest.py | 1 + tests/components/unifi/test_device_tracker.py | 16 +++++- tests/components/unifi/test_switch.py | 16 +++++- tests/components/zha/conftest.py | 8 ++- tests/components/zwave/test_lock.py | 1 + tests/helpers/test_entity_registry.py | 53 +++++++++++++++---- tests/test_config_entries.py | 16 ++++++ 29 files changed, 254 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 21fc55ebafc..d7c8a6ea8e0 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -1,6 +1,9 @@ """Http views to control the config manager.""" +import voluptuous as vol + from homeassistant import config_entries, data_entry_flow from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES +from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView from homeassistant.exceptions import Unauthorized from homeassistant.helpers.data_entry_flow import ( @@ -17,12 +20,17 @@ async def async_setup(hass): hass.http.register_view(ConfigManagerFlowIndexView(hass.config_entries.flow)) hass.http.register_view(ConfigManagerFlowResourceView(hass.config_entries.flow)) hass.http.register_view(ConfigManagerAvailableFlowView) + hass.http.register_view( OptionManagerFlowIndexView(hass.config_entries.options.flow) ) hass.http.register_view( OptionManagerFlowResourceView(hass.config_entries.options.flow) ) + + hass.components.websocket_api.async_register_command(system_options_list) + hass.components.websocket_api.async_register_command(system_options_update) + return True @@ -231,3 +239,40 @@ class OptionManagerFlowResourceView(FlowManagerResourceView): # pylint: disable=no-value-for-parameter return await super().post(request, flow_id) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + {"type": "config_entries/system_options/list", "entry_id": str} +) +async def system_options_list(hass, connection, msg): + """List all system options for a config entry.""" + entry_id = msg["entry_id"] + entry = hass.config_entries.async_get_entry(entry_id) + + if entry: + connection.send_result(msg["id"], entry.system_options.as_dict()) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + "type": "config_entries/system_options/update", + "entry_id": str, + vol.Optional("disable_new_entities"): bool, + } +) +async def system_options_update(hass, connection, msg): + """Update config entry system options.""" + changes = dict(msg) + changes.pop("id") + changes.pop("type") + entry_id = changes.pop("entry_id") + entry = hass.config_entries.async_get_entry(entry_id) + + if entry and changes: + entry.system_options.update(**changes) + + connection.send_result(msg["id"], entry.system_options.as_dict()) diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 1e6198f1a38..9baf1adbcc2 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -127,7 +127,7 @@ async def async_migrate_entry(hass, entry): DOMAIN, unique_id, suggested_object_id=new_id, - config_entry_id=e_entry.config_entry_id, + config_entry=entry, device_id=e_entry.device_id, ) entry.version = 3 diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 46c324110ec..9844aeb9ca6 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -12,13 +12,14 @@ from typing import ( ) import weakref +import attr + from homeassistant import data_entry_flow, loader from homeassistant.core import callback, HomeAssistant from homeassistant.exceptions import HomeAssistantError, ConfigEntryNotReady from homeassistant.setup import async_setup_component, async_process_deps_reqs from homeassistant.util.decorator import Registry - # mypy: allow-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -88,6 +89,7 @@ class ConfigEntry: "title", "data", "options", + "system_options", "source", "connection_class", "state", @@ -104,6 +106,7 @@ class ConfigEntry: data: dict, source: str, connection_class: str, + system_options: dict, options: Optional[dict] = None, entry_id: Optional[str] = None, state: str = ENTRY_STATE_NOT_LOADED, @@ -127,6 +130,9 @@ class ConfigEntry: # Entry options self.options = options or {} + # Entry system options + self.system_options = SystemOptions(**system_options) + # Source of the configuration (user, discovery, cloud) self.source = source @@ -355,6 +361,7 @@ class ConfigEntry: "title": self.title, "data": self.data, "options": self.options, + "system_options": self.system_options.as_dict(), "source": self.source, "connection_class": self.connection_class, } @@ -457,6 +464,8 @@ class ConfigEntries: connection_class=entry.get("connection_class", CONN_CLASS_UNKNOWN), # New in 0.89 options=entry.get("options"), + # New in 0.98 + system_options=entry.get("system_options", {}), ) for entry in config["entries"] ] @@ -580,6 +589,7 @@ class ConfigEntries: title=result["title"], data=result["data"], options={}, + system_options={}, source=flow.context["source"], connection_class=flow.CONNECTION_CLASS, ) @@ -722,3 +732,18 @@ class OptionsFlow(data_entry_flow.FlowHandler): """Base class for config option flows.""" pass + + +@attr.s(slots=True) +class SystemOptions: + """Config entry system options.""" + + disable_new_entities = attr.ib(type=bool, default=False) + + def update(self, *, disable_new_entities): + """Update properties.""" + self.disable_new_entities = disable_new_entities + + def as_dict(self): + """Return dictionary version of this config entrys system options.""" + return {"disable_new_entities": self.disable_new_entities} diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index dd19fac05c8..74351ac50af 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -343,7 +343,7 @@ class EntityPlatform: self.platform_name, entity.unique_id, suggested_object_id=suggested_object_id, - config_entry_id=config_entry_id, + config_entry=self.config_entry, device_id=device_id, known_object_ids=self.entities.keys(), disabled_by=disabled_by, diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 8ef41eef9f8..3d84313a5c6 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -33,6 +33,7 @@ EVENT_ENTITY_REGISTRY_UPDATED = "entity_registry_updated" SAVE_DELAY = 10 _LOGGER = logging.getLogger(__name__) _UNDEF = object() +DISABLED_CONFIG_ENTRY = "config_entry" DISABLED_HASS = "hass" DISABLED_USER = "user" DISABLED_INTEGRATION = "integration" @@ -55,7 +56,13 @@ class RegistryEntry: type=str, default=None, validator=attr.validators.in_( - (DISABLED_HASS, DISABLED_USER, DISABLED_INTEGRATION, None) + ( + DISABLED_HASS, + DISABLED_USER, + DISABLED_INTEGRATION, + DISABLED_CONFIG_ENTRY, + None, + ) ), ) # type: Optional[str] domain = attr.ib(type=str, init=False, repr=False) @@ -132,13 +139,18 @@ class EntityRegistry: unique_id, *, suggested_object_id=None, - config_entry_id=None, + config_entry=None, device_id=None, known_object_ids=None, disabled_by=None, ): """Get entity. Create if it doesn't exist.""" + config_entry_id = None + if config_entry: + config_entry_id = config_entry.entry_id + entity_id = self.async_get_entity_id(domain, platform, unique_id) + if entity_id: return self._async_update_entity( entity_id, @@ -159,6 +171,13 @@ class EntityRegistry: known_object_ids, ) + if ( + disabled_by is None + and config_entry + and config_entry.system_options.disable_new_entities + ): + disabled_by = DISABLED_INTEGRATION + entity = RegistryEntry( entity_id=entity_id, config_entry_id=config_entry_id, diff --git a/tests/common.py b/tests/common.py index f7816bf2192..0e2f701c210 100644 --- a/tests/common.py +++ b/tests/common.py @@ -665,6 +665,7 @@ class MockConfigEntry(config_entries.ConfigEntry): title="Mock Title", state=None, options={}, + system_options={}, connection_class=config_entries.CONN_CLASS_UNKNOWN, ): """Initialize a mock config entry.""" @@ -672,6 +673,7 @@ class MockConfigEntry(config_entries.ConfigEntry): "entry_id": entry_id or uuid.uuid4().hex, "domain": domain, "data": data or {}, + "system_options": system_options, "options": options, "version": version, "title": title, diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index 6a58812cbaf..8e5a6f9675d 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -57,6 +57,7 @@ async def setup_device(hass): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, options=ENTRY_OPTIONS, ) device = axis.AxisNetworkDevice(hass, config_entry) diff --git a/tests/components/axis/test_camera.py b/tests/components/axis/test_camera.py index f4871357d0e..027dc42748e 100644 --- a/tests/components/axis/test_camera.py +++ b/tests/components/axis/test_camera.py @@ -41,6 +41,7 @@ async def setup_device(hass): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, options=ENTRY_OPTIONS, ) device = axis.AxisNetworkDevice(hass, config_entry) diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index 6595f172ef0..3469106c436 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -59,6 +59,7 @@ async def setup_device(hass): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, options=ENTRY_OPTIONS, ) device = axis.AxisNetworkDevice(hass, config_entry) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 13cd8da0597..f0815e7ede8 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -584,3 +584,47 @@ async def test_two_step_options_flow(hass, client): "description": None, "description_placeholders": None, } + + +async def test_list_system_options(hass, hass_ws_client): + """Test that we can list an entries system options.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + + entry = MockConfigEntry(domain="demo") + entry.add_to_hass(hass) + + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/system_options/list", + "entry_id": entry.entry_id, + } + ) + response = await ws_client.receive_json() + + assert response["success"] + assert response["result"] == {"disable_new_entities": False} + + +async def test_update_system_options(hass, hass_ws_client): + """Test that we can update system options.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + + entry = MockConfigEntry(domain="demo") + entry.add_to_hass(hass) + + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/system_options/update", + "entry_id": entry.entry_id, + "disable_new_entities": True, + } + ) + response = await ws_client.receive_json() + + assert response["success"] + assert response["result"]["disable_new_entities"] + assert entry.system_options.disable_new_entities diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 9e6d4f571ea..acf06728d0d 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -59,7 +59,8 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, - ENTRY_OPTIONS, + system_options={}, + options=ENTRY_OPTIONS, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 68d1957f97e..f4972564a8e 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -71,7 +71,8 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, - ENTRY_OPTIONS, + system_options={}, + options=ENTRY_OPTIONS, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(hass.loop, session, **config_entry.data) diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index ee68744f999..f264877b77a 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -63,6 +63,7 @@ async def setup_gateway(hass, data): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index ee81e476a12..afe7ca445e5 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -90,7 +90,8 @@ async def setup_gateway(hass, data, allow_deconz_groups=True): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, - ENTRY_OPTIONS, + system_options={}, + options=ENTRY_OPTIONS, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index 48287d5200a..074e943548d 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -46,6 +46,7 @@ async def setup_gateway(hass, data): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 21efd768be2..fa1ba175ed5 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -103,7 +103,8 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, - ENTRY_OPTIONS, + system_options={}, + options=ENTRY_OPTIONS, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 433386a1751..746d1b6342c 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -67,6 +67,7 @@ async def setup_gateway(hass, data): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) diff --git a/tests/components/homekit_controller/test_storage.py b/tests/components/homekit_controller/test_storage.py index 3c7bd4c8e2c..4fcf035ae48 100644 --- a/tests/components/homekit_controller/test_storage.py +++ b/tests/components/homekit_controller/test_storage.py @@ -97,6 +97,7 @@ async def test_storage_is_removed_on_config_entry_removal(hass, utcnow): pairing_data, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, ) assert hkid in hass.data[ENTITY_MAP].storage_data diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 4038f4f9e87..1c891b9c840 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -221,6 +221,7 @@ async def setup_bridge(hass, mock_bridge): {"host": "mock-host"}, "test", config_entries.CONN_CLASS_LOCAL_POLL, + system_options={}, ) await hass.config_entries.async_forward_entry_setup(config_entry, "light") # To flush out the service call to update the group diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index c78192d2572..72ac816483a 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -306,6 +306,7 @@ async def setup_bridge(hass, mock_bridge, hostname=None): {"host": hostname}, "test", config_entries.CONN_CLASS_LOCAL_POLL, + system_options={}, ) await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor") await hass.config_entries.async_forward_entry_setup(config_entry, "sensor") diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index a2eca90e60e..5b6d6f87cd5 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -142,7 +142,7 @@ async def test_config_flow_entry_migrate(hass): "media_player", "ps4", MOCK_UNIQUE_ID, - config_entry_id=MOCK_ENTRY_ID, + config_entry=mock_entry, device_id=MOCK_DEVICE_ID, ) assert len(mock_e_registry.entities) == 1 diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index b7f4ff53ec8..f7e2f865cb2 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -335,7 +335,7 @@ async def test_device_info_is_assummed(hass): mock_unique_id = ps4.format_unique_id(MOCK_CREDS, MOCK_HOST_ID) mock_e_registry = mock_registry(hass) mock_e_registry.async_get_or_create( - "media_player", DOMAIN, mock_unique_id, config_entry_id=MOCK_ENTRY_ID + "media_player", DOMAIN, mock_unique_id, config_entry=MOCK_CONFIG ) mock_entity_id = mock_e_registry.async_get_entity_id( "media_player", DOMAIN, mock_unique_id diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 3f93c442985..b3b172e7606 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -56,6 +56,7 @@ async def setup_platform(hass, platform: str, *, devices=None, scenes=None): {CONF_INSTALLED_APP_ID: str(uuid4())}, SOURCE_USER, CONN_CLASS_CLOUD_PUSH, + system_options={}, ) broker = DeviceBroker( hass, config_entry, Mock(), Mock(), devices or [], scenes or [] diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index d5783e58818..0da72c924c4 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -145,6 +145,7 @@ async def setup_controller(hass, mock_controller): "test", config_entries.CONN_CLASS_LOCAL_POLL, entry_id=1, + system_options={}, ) mock_controller.config_entry = config_entry @@ -235,20 +236,31 @@ async def test_restoring_client(hass, mock_controller): mock_controller.mock_client_all_responses.append([CLIENT_1]) mock_controller.unifi_config = {unifi.CONF_BLOCK_CLIENT: True} + config_entry = config_entries.ConfigEntry( + 1, + unifi.DOMAIN, + "Mock Title", + ENTRY_CONFIG, + "test", + config_entries.CONN_CLASS_LOCAL_POLL, + entry_id=1, + system_options={}, + ) + registry = await entity_registry.async_get_registry(hass) registry.async_get_or_create( device_tracker.DOMAIN, unifi_dt.UNIFI_DOMAIN, "{}-mock-site".format(CLIENT_1["mac"]), suggested_object_id=CLIENT_1["hostname"], - config_entry_id=1, + config_entry=config_entry, ) registry.async_get_or_create( device_tracker.DOMAIN, unifi_dt.UNIFI_DOMAIN, "{}-mock-site".format(CLIENT_2["mac"]), suggested_object_id=CLIENT_2["hostname"], - config_entry_id=1, + config_entry=config_entry, ) await setup_controller(hass, mock_controller) diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index f84efa5dada..05c58abbc94 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -262,6 +262,7 @@ async def setup_controller(hass, mock_controller): "test", config_entries.CONN_CLASS_LOCAL_POLL, entry_id=1, + system_options={}, ) mock_controller.config_entry = config_entry @@ -468,20 +469,31 @@ async def test_restoring_client(hass, mock_controller): mock_controller.mock_client_all_responses.append([CLIENT_1]) mock_controller.unifi_config = {unifi.CONF_BLOCK_CLIENT: ["random mac"]} + config_entry = config_entries.ConfigEntry( + 1, + unifi.DOMAIN, + "Mock Title", + ENTRY_CONFIG, + "test", + config_entries.CONN_CLASS_LOCAL_POLL, + entry_id=1, + system_options={}, + ) + registry = await entity_registry.async_get_registry(hass) registry.async_get_or_create( switch.DOMAIN, unifi.DOMAIN, "poe-{}".format(CLIENT_1["mac"]), suggested_object_id=CLIENT_1["hostname"], - config_entry_id=1, + config_entry=config_entry, ) registry.async_get_or_create( switch.DOMAIN, unifi.DOMAIN, "poe-{}".format(CLIENT_2["mac"]), suggested_object_id=CLIENT_2["hostname"], - config_entry_id=1, + config_entry=config_entry, ) await setup_controller(hass, mock_controller) diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 5433fc62a61..b836c55df17 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -14,7 +14,13 @@ from homeassistant.components.zha.core.store import async_get_registry def config_entry_fixture(hass): """Fixture representing a config entry.""" config_entry = config_entries.ConfigEntry( - 1, DOMAIN, "Mock Title", {}, "test", config_entries.CONN_CLASS_LOCAL_PUSH + 1, + DOMAIN, + "Mock Title", + {}, + "test", + config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, ) return config_entry diff --git a/tests/components/zwave/test_lock.py b/tests/components/zwave/test_lock.py index 9c6ab7835cd..4a32c3fb07c 100644 --- a/tests/components/zwave/test_lock.py +++ b/tests/components/zwave/test_lock.py @@ -287,6 +287,7 @@ async def setup_ozw(hass, mock_openzwave): {"usb_path": "mock-path", "network_key": "mock-key"}, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, ) await hass.config_entries.async_forward_entry_setup(config_entry, "lock") await hass.async_block_till_done() diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index ce05e914b3d..aee6b6f19a3 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -8,7 +8,7 @@ import pytest from homeassistant.core import valid_entity_id, callback from homeassistant.helpers import entity_registry -from tests.common import mock_registry, flush_store +from tests.common import MockConfigEntry, mock_registry, flush_store YAML__OPEN_PATH = "homeassistant.util.yaml.loader.open" @@ -88,9 +88,11 @@ def test_create_triggers_save(hass, registry): async def test_loading_saving_data(hass, registry): """Test that we load/save data correctly.""" + mock_config = MockConfigEntry(domain="light") + orig_entry1 = registry.async_get_or_create("light", "hue", "1234") orig_entry2 = registry.async_get_or_create( - "light", "hue", "5678", config_entry_id="mock-id" + "light", "hue", "5678", config_entry=mock_config ) assert len(registry.entities) == 2 @@ -104,7 +106,7 @@ async def test_loading_saving_data(hass, registry): assert list(registry.entities) == list(registry2.entities) new_entry1 = registry.async_get_or_create("light", "hue", "1234") new_entry2 = registry.async_get_or_create( - "light", "hue", "5678", config_entry_id="mock-id" + "light", "hue", "5678", config_entry=mock_config ) assert orig_entry1 == new_entry1 @@ -198,11 +200,14 @@ def test_async_get_entity_id(registry): async def test_updating_config_entry_id(hass, registry, update_events): """Test that we update config entry id in registry.""" + mock_config_1 = MockConfigEntry(domain="light", entry_id="mock-id-1") entry = registry.async_get_or_create( - "light", "hue", "5678", config_entry_id="mock-id-1" + "light", "hue", "5678", config_entry=mock_config_1 ) + + mock_config_2 = MockConfigEntry(domain="light", entry_id="mock-id-2") entry2 = registry.async_get_or_create( - "light", "hue", "5678", config_entry_id="mock-id-2" + "light", "hue", "5678", config_entry=mock_config_2 ) assert entry.entity_id == entry2.entity_id assert entry2.config_entry_id == "mock-id-2" @@ -218,8 +223,10 @@ async def test_updating_config_entry_id(hass, registry, update_events): async def test_removing_config_entry_id(hass, registry, update_events): """Test that we update config entry id in registry.""" + mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") + entry = registry.async_get_or_create( - "light", "hue", "5678", config_entry_id="mock-id-1" + "light", "hue", "5678", config_entry=mock_config ) assert entry.config_entry_id == "mock-id-1" registry.async_clear_config_entry("mock-id-1") @@ -237,6 +244,8 @@ async def test_removing_config_entry_id(hass, registry, update_events): async def test_migration(hass): """Test migration from old data to new.""" + mock_config = MockConfigEntry(domain="test-platform", entry_id="test-config-id") + old_conf = { "light.kitchen": { "config_entry_id": "test-config-id", @@ -256,7 +265,7 @@ async def test_migration(hass): domain="light", platform="test-platform", unique_id="test-unique", - config_entry_id="test-config-id", + config_entry=mock_config, ) assert entry.name == "Test Name" assert entry.disabled_by == "hass" @@ -326,8 +335,10 @@ async def test_loading_race_condition(hass): async def test_update_entity_unique_id(registry): """Test entity's unique_id is updated.""" + mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") + entry = registry.async_get_or_create( - "light", "hue", "5678", config_entry_id="mock-id-1" + "light", "hue", "5678", config_entry=mock_config ) new_unique_id = "1234" with patch.object(registry, "async_schedule_save") as mock_schedule_save: @@ -341,11 +352,12 @@ async def test_update_entity_unique_id(registry): async def test_update_entity_unique_id_conflict(registry): """Test migration raises when unique_id already in use.""" + mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") entry = registry.async_get_or_create( - "light", "hue", "5678", config_entry_id="mock-id-1" + "light", "hue", "5678", config_entry=mock_config ) entry2 = registry.async_get_or_create( - "light", "hue", "1234", config_entry_id="mock-id-1" + "light", "hue", "1234", config_entry=mock_config ) with patch.object( registry, "async_schedule_save" @@ -356,8 +368,9 @@ async def test_update_entity_unique_id_conflict(registry): async def test_update_entity(registry): """Test updating entity.""" + mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") entry = registry.async_get_or_create( - "light", "hue", "5678", config_entry_id="mock-id-1" + "light", "hue", "5678", config_entry=mock_config ) for attr_name, new_value in ( @@ -386,3 +399,21 @@ async def test_disabled_by(registry): entry2 = registry.async_get_or_create("light", "hue", "1234") assert entry2.disabled_by is None + + +async def test_disabled_by_system_options(registry): + """Test system options setting disabled_by.""" + mock_config = MockConfigEntry( + domain="light", + entry_id="mock-id-1", + system_options={"disable_new_entities": True}, + ) + entry = registry.async_get_or_create( + "light", "hue", "AAAA", config_entry=mock_config + ) + assert entry.disabled_by == "integration" + + entry2 = registry.async_get_or_create( + "light", "hue", "BBBB", config_entry=mock_config, disabled_by="user" + ) + assert entry2.disabled_by == "user" diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 361feccc34d..6c1b00693dd 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -596,6 +596,22 @@ async def test_updating_entry_data(manager): assert entry.data == {"second": True} +async def test_updating_entry_system_options(manager): + """Test that we can update an entry data.""" + entry = MockConfigEntry( + domain="test", + data={"first": True}, + state=config_entries.ENTRY_STATE_SETUP_ERROR, + system_options={"disable_new_entities": True}, + ) + entry.add_to_manager(manager) + + assert entry.system_options.disable_new_entities + + entry.system_options.update(disable_new_entities=False) + assert not entry.system_options.disable_new_entities + + async def test_update_entry_options_and_trigger_listener(hass, manager): """Test that we can update entry options and trigger listener.""" entry = MockConfigEntry(domain="test", options={"first": True}) From ef8bc78c537c7a4084c2bb92686fa4876a2b3c6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 18 Aug 2019 10:55:19 +0200 Subject: [PATCH 188/273] Remove the ruter integration (#26041) --- .coveragerc | 1 - CODEOWNERS | 1 - homeassistant/components/ruter/__init__.py | 1 - homeassistant/components/ruter/manifest.json | 12 --- homeassistant/components/ruter/sensor.py | 93 -------------------- requirements_all.txt | 3 - 6 files changed, 111 deletions(-) delete mode 100644 homeassistant/components/ruter/__init__.py delete mode 100644 homeassistant/components/ruter/manifest.json delete mode 100644 homeassistant/components/ruter/sensor.py diff --git a/.coveragerc b/.coveragerc index 248e242faaf..11b5b91ae22 100644 --- a/.coveragerc +++ b/.coveragerc @@ -534,7 +534,6 @@ omit = homeassistant/components/rtorrent/sensor.py homeassistant/components/russound_rio/media_player.py homeassistant/components/russound_rnet/media_player.py - homeassistant/components/ruter/sensor.py homeassistant/components/sabnzbd/* homeassistant/components/satel_integra/* homeassistant/components/scrape/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 07501e27f5e..9c2fa006a13 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -222,7 +222,6 @@ homeassistant/components/repetier/* @MTrab homeassistant/components/rfxtrx/* @danielhiversen homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roomba/* @pschmitt -homeassistant/components/ruter/* @ludeeus homeassistant/components/scene/* @home-assistant/core homeassistant/components/scrape/* @fabaff homeassistant/components/script/* @home-assistant/core diff --git a/homeassistant/components/ruter/__init__.py b/homeassistant/components/ruter/__init__.py deleted file mode 100644 index 84e25904d9e..00000000000 --- a/homeassistant/components/ruter/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The ruter component.""" diff --git a/homeassistant/components/ruter/manifest.json b/homeassistant/components/ruter/manifest.json deleted file mode 100644 index 57688d0e025..00000000000 --- a/homeassistant/components/ruter/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "domain": "ruter", - "name": "Ruter", - "documentation": "https://www.home-assistant.io/components/ruter", - "requirements": [ - "pyruter==1.1.0" - ], - "dependencies": [], - "codeowners": [ - "@ludeeus" - ] -} diff --git a/homeassistant/components/ruter/sensor.py b/homeassistant/components/ruter/sensor.py deleted file mode 100644 index ba4d7368628..00000000000 --- a/homeassistant/components/ruter/sensor.py +++ /dev/null @@ -1,93 +0,0 @@ -"""A sensor to provide information about next departures from Ruter.""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.aiohttp_client import async_get_clientsession - -_LOGGER = logging.getLogger(__name__) - -CONF_STOP_ID = "stop_id" -CONF_DESTINATION = "destination" -CONF_OFFSET = "offset" - -DEFAULT_NAME = "Ruter" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_STOP_ID): cv.positive_int, - vol.Optional(CONF_DESTINATION): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OFFSET, default=0): cv.positive_int, - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Create the sensor.""" - from pyruter.api import Departures - - _LOGGER.warning( - "The API used in this sensor is shutting down soon, " - "you should consider starting to use the " - "'entur_public_transport' sensor instead" - ) - stop_id = config[CONF_STOP_ID] - destination = config.get(CONF_DESTINATION) - name = config[CONF_NAME] - offset = config[CONF_OFFSET] - - session = async_get_clientsession(hass) - ruter = Departures(hass.loop, stop_id, destination, session) - sensor = [RuterSensor(ruter, name, offset)] - async_add_entities(sensor, True) - - -class RuterSensor(Entity): - """Representation of a Ruter sensor.""" - - def __init__(self, ruter, name, offset): - """Initialize the sensor.""" - self.ruter = ruter - self._attributes = {} - self._name = name - self._offset = offset - self._state = None - - async def async_update(self): - """Get the latest data from the Ruter API.""" - await self.ruter.get_departures() - if self.ruter.departures is None: - _LOGGER.error("No data received from Ruter.") - return - try: - data = self.ruter.departures[self._offset] - self._state = data["time"] - self._attributes["line"] = data["line"] - self._attributes["destination"] = data["destination"] - except (KeyError, IndexError) as error: - _LOGGER.debug("Error getting data from Ruter, %s", error) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def icon(self): - """Return the icon of the sensor.""" - return "mdi:bus" - - @property - def device_state_attributes(self): - """Return attributes for the sensor.""" - return self._attributes diff --git a/requirements_all.txt b/requirements_all.txt index b652cf81017..79a2d984abd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1361,9 +1361,6 @@ pyrecswitch==1.0.2 # homeassistant.components.repetier pyrepetier==3.0.5 -# homeassistant.components.ruter -pyruter==1.1.0 - # homeassistant.components.sabnzbd pysabnzbd==1.1.0 From 2a39d1209c4a8692caa156ed936ab3cdee3f0bd6 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 18 Aug 2019 11:14:07 +0200 Subject: [PATCH 189/273] Add support of new Xiaomi Aqara Curtain models (#25942) * Add support of new Xiaomi Aqara Curtain models (curtain.aq2, curtain.hagl04) (Closes: #25711) * Fix requirements_all --- homeassistant/components/xiaomi_aqara/cover.py | 2 +- homeassistant/components/xiaomi_aqara/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/cover.py b/homeassistant/components/xiaomi_aqara/cover.py index 2b6301f4133..da6b24d616a 100644 --- a/homeassistant/components/xiaomi_aqara/cover.py +++ b/homeassistant/components/xiaomi_aqara/cover.py @@ -19,7 +19,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items(): for device in gateway.devices["cover"]: model = device["model"] - if model == "curtain": + if model in ["curtain", "curtain.aq2", "curtain.hagl04"]: if "proto" not in device or int(device["proto"][0:1]) == 1: data_key = DATA_KEY_PROTO_V1 else: diff --git a/homeassistant/components/xiaomi_aqara/manifest.json b/homeassistant/components/xiaomi_aqara/manifest.json index 8620b1dc34c..36da259f82e 100644 --- a/homeassistant/components/xiaomi_aqara/manifest.json +++ b/homeassistant/components/xiaomi_aqara/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi aqara", "documentation": "https://www.home-assistant.io/components/xiaomi_aqara", "requirements": [ - "PyXiaomiGateway==0.12.3" + "PyXiaomiGateway==0.12.4" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 79a2d984abd..3b00f17f8c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -75,7 +75,7 @@ PyRMVtransport==0.1.3 PyTransportNSW==0.1.1 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.12.3 +PyXiaomiGateway==0.12.4 # homeassistant.components.mcp23017 # homeassistant.components.rpi_gpio From 10d63e46d7b654ed911bb2af7c92840b76b1eac0 Mon Sep 17 00:00:00 2001 From: ThaSiouL Date: Sun, 18 Aug 2019 17:08:26 +0200 Subject: [PATCH 190/273] Fritz device tracker: set 'scanning' log to debug (#26043) Changed the logging level for the polling of the fritz device tracker from info to debug. The message was logged up to 6 times per minute and bloated the log file. --- homeassistant/components/fritz/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index f34cda67ad9..afe0aa3ed02 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -85,6 +85,6 @@ class FritzBoxScanner(DeviceScanner): if not self.success_init: return False - _LOGGER.info("Scanning") + _LOGGER.debug("Scanning") self.last_results = self.fritz_box.get_hosts_info() return True From d4981a1143aea2a571b557ff3c0e9fe58b3ccfbf Mon Sep 17 00:00:00 2001 From: Robert Dunmire III Date: Sun, 18 Aug 2019 11:14:54 -0400 Subject: [PATCH 191/273] Fix Mikrotik ARP ping (#25965) * Reuse ssl_wraper * Fix arp_ping * Restore debug log * Fix attributes --- homeassistant/components/mikrotik/__init__.py | 14 +++++++++----- homeassistant/components/mikrotik/const.py | 1 + .../components/mikrotik/device_tracker.py | 8 ++++---- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index 236892a98b9..aacd3c65b3e 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -18,6 +18,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from .const import ( + NAME, DOMAIN, HOSTS, MTK_LOGIN_PLAIN, @@ -121,6 +122,7 @@ class MikrotikClient: self._password = password self._login_method = login_method self._encoding = encoding + self._ssl_wrapper = None self.hostname = None self._client = None self._connected = False @@ -137,10 +139,12 @@ class MikrotikClient: } if self._use_ssl: - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - kwargs["ssl_wrapper"] = ssl_context.wrap_socket + if self._ssl_wrapper is None: + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + self._ssl_wrapper = ssl_context.wrap_socket + kwargs["ssl_wrapper"] = self._ssl_wrapper try: self._client = librouteros.connect( @@ -163,7 +167,7 @@ class MikrotikClient: def get_hostname(self): """Return device host name.""" data = self.command(MIKROTIK_SERVICES[IDENTITY]) - return data[0]["name"] if data else None + return data[0][NAME] if data else None def connected(self): """Return connected boolean.""" diff --git a/homeassistant/components/mikrotik/const.py b/homeassistant/components/mikrotik/const.py index 4f511d6b418..bd26b02fe1b 100644 --- a/homeassistant/components/mikrotik/const.py +++ b/homeassistant/components/mikrotik/const.py @@ -12,6 +12,7 @@ CONF_LOGIN_METHOD = "login_method" CONF_ENCODING = "encoding" DEFAULT_ENCODING = "utf-8" +NAME = "name" INFO = "info" IDENTITY = "identity" ARP = "arp" diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 47d3fab28ad..6c3fb559cba 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -132,8 +132,9 @@ class MikrotikScanner(DeviceScanner): if self.arp_ping and self.devices_arp: if mac not in self.devices_arp: continue + ip_address = self.devices_arp[mac]["address"] interface = self.devices_arp[mac]["interface"] - if not self.do_arp_ping(mac, interface): + if not self.do_arp_ping(ip_address, interface): continue attrs = {} @@ -148,20 +149,19 @@ class MikrotikScanner(DeviceScanner): for attr in ATTR_DEVICE_TRACKER: if attr in device and device[attr] is not None: attrs[slugify(attr)] = device[attr] - attrs["scanner_type"] = self.method attrs["scanner_host"] = self.host attrs["scanner_hostname"] = self.hostname self.device_tracker[mac] = attrs - def do_arp_ping(self, mac, interface): + def do_arp_ping(self, ip_address, interface): """Attempt to arp ping MAC address via interface.""" params = { "arp-ping": "yes", "interval": "100ms", "count": 3, "interface": interface, - "address": mac, + "address": ip_address, } cmd = "/ping" data = self.api.command(cmd, params) From 3463cc9cbdc99ac90e6df21dfa9b507bd63279cf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 18 Aug 2019 11:36:23 -0700 Subject: [PATCH 192/273] Make sure config flows extend ConfigFlow base class (#26051) --- homeassistant/components/ipma/config_flow.py | 4 ++-- homeassistant/components/met/config_flow.py | 4 ++-- homeassistant/components/smhi/config_flow.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ipma/config_flow.py b/homeassistant/components/ipma/config_flow.py index c0bc383abc0..d1532066f68 100644 --- a/homeassistant/components/ipma/config_flow.py +++ b/homeassistant/components/ipma/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure IPMA component.""" import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -9,7 +9,7 @@ from .const import DOMAIN, HOME_LOCATION_NAME @config_entries.HANDLERS.register(DOMAIN) -class IpmaFlowHandler(data_entry_flow.FlowHandler): +class IpmaFlowHandler(config_entries.ConfigFlow): """Config flow for IPMA component.""" VERSION = 1 diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index 9088d958cf0..e903c717e64 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure Met component.""" import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -18,7 +18,7 @@ def configured_instances(hass): @config_entries.HANDLERS.register(DOMAIN) -class MetFlowHandler(data_entry_flow.FlowHandler): +class MetFlowHandler(config_entries.ConfigFlow): """Config flow for Met component.""" VERSION = 1 diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py index a68c8293a9f..3b60cb66165 100644 --- a/homeassistant/components/smhi/config_flow.py +++ b/homeassistant/components/smhi/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure SMHI component.""" import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client @@ -21,7 +21,7 @@ def smhi_locations(hass: HomeAssistant): @config_entries.HANDLERS.register(DOMAIN) -class SmhiFlowHandler(data_entry_flow.FlowHandler): +class SmhiFlowHandler(config_entries.ConfigFlow): """Config flow for SMHI component.""" VERSION = 1 From 34f7bb5b8c06d4dd96c36f3f84d09fdb9a48679b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 19 Aug 2019 12:09:56 +0200 Subject: [PATCH 193/273] Revert "Fix bmw_connected_drive and eq3btsmart components by updating their dependencies (#26012)" (#26065) This reverts commit 9e5243929e3821daa44ecb592406fa4a4da88cd1. --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- homeassistant/components/eq3btsmart/manifest.json | 2 +- requirements_all.txt | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) mode change 100755 => 100644 homeassistant/components/bmw_connected_drive/manifest.json mode change 100755 => 100644 homeassistant/components/eq3btsmart/manifest.json diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json old mode 100755 new mode 100644 index ad5f712f817..eec81aa6525 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -3,7 +3,7 @@ "name": "Bmw connected drive", "documentation": "https://www.home-assistant.io/components/bmw_connected_drive", "requirements": [ - "bimmer_connected==0.5.6" + "bimmer_connected==0.5.3" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json old mode 100755 new mode 100644 index 26d732fc927..6d13c79bcec --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/components/eq3btsmart", "requirements": [ "construct==2.9.45", - "python-eq3bt==0.1.11" + "python-eq3bt==0.1.9" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 3b00f17f8c2..a05fc86ff0f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -266,7 +266,7 @@ beautifulsoup4==4.8.0 bellows-homeassistant==0.9.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.5.6 +bimmer_connected==0.5.3 # homeassistant.components.bizkaibus bizkaibus==0.1.1 @@ -1443,7 +1443,7 @@ python-digitalocean==1.13.2 python-ecobee-api==0.0.21 # homeassistant.components.eq3btsmart -# python-eq3bt==0.1.11 +# python-eq3bt==0.1.9 # homeassistant.components.etherscan python-etherscan-api==0.0.3 From 6b80df9652998978ebfe6e710936e6c51b109fe0 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 19 Aug 2019 12:15:41 +0200 Subject: [PATCH 194/273] Bump nabucasa-cloud to 0.17 (#26066) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 58739bededc..3daeac43da9 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Cloud", "documentation": "https://www.home-assistant.io/components/cloud", - "requirements": ["hass-nabucasa==0.16"], + "requirements": ["hass-nabucasa==0.17"], "dependencies": ["http", "webhook"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a898dc497d8..1abb8d1d822 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2019.6.16 contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 -hass-nabucasa==0.16 +hass-nabucasa==0.17 home-assistant-frontend==20190815.0 importlib-metadata==0.19 jinja2>=2.10.1 diff --git a/requirements_all.txt b/requirements_all.txt index a05fc86ff0f..c7b02188df6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -597,7 +597,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.16 +hass-nabucasa==0.17 # homeassistant.components.mqtt hbmqtt==0.9.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fcdf314727e..3a81c3ef290 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -165,7 +165,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.16 +hass-nabucasa==0.17 # homeassistant.components.mqtt hbmqtt==0.9.4 From 15ab004e98413ae4e3e75c6329f2dcd289076118 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Mon, 19 Aug 2019 14:00:47 +0200 Subject: [PATCH 195/273] fix alarm webhooks (#26062) --- homeassistant/components/point/alarm_control_panel.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py index 7dc7e164788..4a0db111b7d 100644 --- a/homeassistant/components/point/alarm_control_panel.py +++ b/homeassistant/components/point/alarm_control_panel.py @@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) EVENT_MAP = { "off": STATE_ALARM_DISARMED, - "alarm_silenced": STATE_ALARM_ARMED_AWAY, + "alarm_silenced": STATE_ALARM_DISARMED, "alarm_grace_period_expired": STATE_ALARM_TRIGGERED, } @@ -63,11 +63,14 @@ class MinutPointAlarmControl(AlarmControlPanel): """Process new event from the webhook.""" _type = data.get("event", {}).get("type") _device_id = data.get("event", {}).get("device_id") - if _device_id not in self._home["devices"] or _type not in EVENT_MAP: + _changed_by = data.get("event", {}).get("user_id") + if ( + _device_id not in self._home["devices"] and _type not in EVENT_MAP + ) and _type != "alarm_silenced": # alarm_silenced does not have device_id return _LOGGER.debug("Received webhook: %s", _type) - self._home["alarm_status"] = EVENT_MAP[_type] - self._changed_by = _device_id + self._home["alarm_status"] = _type + self._changed_by = _changed_by self.async_schedule_update_ha_state() @property From 75e18d428252ab2314a00cdd23d6c252e45eac4a Mon Sep 17 00:00:00 2001 From: Philipp Danner Date: Mon, 19 Aug 2019 14:29:26 +0200 Subject: [PATCH 196/273] Add Keba charging station/wallbox as component (#24484) * Add Keba charging station wallbox component * Added start/stop commands (ena 0 and ena 1) * added refresh_interval parameter and fixed authorization * fixed max line length * deactivate failsafe mode if not set in configuration * extracted I/O code to pypi library * updated services.yaml * pinned version of requirements * fixed typos, indent and comments * simplified sensor generation, fixed unique_id and name of sensors * cleaned up data extraction * flake8 fixes * added fast polling, fixed unique_id, code cleanup * updated requirements * fixed pylint * integrated code styling suggestions * fixed pylint * code style changes according to suggestions and pylint fixes * formatted with black * clarefied variables * Update homeassistant/components/keba/__init__.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/keba/__init__.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/keba/__init__.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/keba/__init__.py Co-Authored-By: Martin Hjelmare * fixed behaviour if no charging station was found * fix pylint * Update homeassistant/components/keba/__init__.py Co-Authored-By: Martin Hjelmare --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/keba/__init__.py | 229 ++++++++++++++++++ .../components/keba/binary_sensor.py | 108 +++++++++ homeassistant/components/keba/lock.py | 69 ++++++ homeassistant/components/keba/manifest.json | 10 + homeassistant/components/keba/sensor.py | 109 +++++++++ homeassistant/components/keba/services.yaml | 56 +++++ requirements_all.txt | 3 + 9 files changed, 586 insertions(+) create mode 100644 homeassistant/components/keba/__init__.py create mode 100644 homeassistant/components/keba/binary_sensor.py create mode 100644 homeassistant/components/keba/lock.py create mode 100644 homeassistant/components/keba/manifest.json create mode 100644 homeassistant/components/keba/sensor.py create mode 100644 homeassistant/components/keba/services.yaml diff --git a/.coveragerc b/.coveragerc index 11b5b91ae22..d8153a7635c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -308,6 +308,7 @@ omit = homeassistant/components/joaoapps_join/* homeassistant/components/juicenet/* homeassistant/components/kankun/switch.py + homeassistant/components/keba/* homeassistant/components/keenetic_ndms2/device_tracker.py homeassistant/components/keyboard/* homeassistant/components/keyboard_remote/* diff --git a/CODEOWNERS b/CODEOWNERS index 9c2fa006a13..f6b9e79b8bd 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -143,6 +143,7 @@ homeassistant/components/ipma/* @dgomes homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/jewish_calendar/* @tsvi +homeassistant/components/keba/* @dannerph homeassistant/components/knx/* @Julius2342 homeassistant/components/kodi/* @armills homeassistant/components/konnected/* @heythisisnate diff --git a/homeassistant/components/keba/__init__.py b/homeassistant/components/keba/__init__.py new file mode 100644 index 00000000000..5a9a49a005a --- /dev/null +++ b/homeassistant/components/keba/__init__.py @@ -0,0 +1,229 @@ +"""Support for KEBA charging stations.""" +import asyncio +import logging + +from keba_kecontact.connection import KebaKeContact +import voluptuous as vol + +from homeassistant.const import CONF_HOST +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "keba" +SUPPORTED_COMPONENTS = ["binary_sensor", "sensor", "lock"] + +CONF_RFID = "rfid" +CONF_FS = "failsafe" +CONF_FS_TIMEOUT = "failsafe_timeout" +CONF_FS_FALLBACK = "failsafe_fallback" +CONF_FS_PERSIST = "failsafe_persist" +CONF_FS_INTERVAL = "refresh_interval" + +MAX_POLLING_INTERVAL = 5 # in seconds +MAX_FAST_POLLING_COUNT = 4 + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_RFID, default="00845500"): cv.string, + vol.Optional(CONF_FS, default=False): cv.boolean, + vol.Optional(CONF_FS_TIMEOUT, default=30): cv.positive_int, + vol.Optional(CONF_FS_FALLBACK, default=6): cv.positive_int, + vol.Optional(CONF_FS_PERSIST, default=0): cv.positive_int, + vol.Optional(CONF_FS_INTERVAL, default=5): cv.positive_int, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + +_SERVICE_MAP = { + "request_data": "request_data", + "set_energy": "async_set_energy", + "set_current": "async_set_current", + "authorize": "async_start", + "deauthorize": "async_stop", + "enable": "async_enable_ev", + "disable": "async_disable_ev", + "set_failsafe": "async_set_failsafe", +} + + +async def async_setup(hass, config): + """Check connectivity and version of KEBA charging station.""" + host = config[DOMAIN][CONF_HOST] + rfid = config[DOMAIN][CONF_RFID] + refresh_interval = config[DOMAIN][CONF_FS_INTERVAL] + keba = KebaHandler(hass, host, rfid, refresh_interval) + hass.data[DOMAIN] = keba + + # Wait for KebaHandler setup complete (initial values loaded) + if not await keba.setup(): + _LOGGER.error("Could not find a charging station at %s", host) + return False + + # Set failsafe mode at start up of home assistant + failsafe = config[DOMAIN][CONF_FS] + timeout = config[DOMAIN][CONF_FS_TIMEOUT] if failsafe else 0 + fallback = config[DOMAIN][CONF_FS_FALLBACK] if failsafe else 0 + persist = config[DOMAIN][CONF_FS_PERSIST] if failsafe else 0 + try: + hass.loop.create_task(keba.set_failsafe(timeout, fallback, persist)) + except ValueError as ex: + _LOGGER.warning("Could not set failsafe mode %s", ex) + + # Register services to hass + async def execute_service(call): + """Execute a service to KEBA charging station. + + This must be a member function as we need access to the keba + object here. + """ + function_name = _SERVICE_MAP[call.service] + function_call = getattr(keba, function_name) + await function_call(call.data) + + for service in _SERVICE_MAP: + hass.services.async_register(DOMAIN, service, execute_service) + + # Load components + for domain in SUPPORTED_COMPONENTS: + hass.async_create_task( + discovery.async_load_platform(hass, domain, DOMAIN, {}, config) + ) + + # Start periodic polling of charging station data + keba.start_periodic_request() + + return True + + +class KebaHandler(KebaKeContact): + """Representation of a KEBA charging station connection.""" + + def __init__(self, hass, host, rfid, refresh_interval): + """Constructor.""" + super().__init__(host, self.hass_callback) + + self._update_listeners = [] + self._hass = hass + self.rfid = rfid + self.device_name = "keba_wallbox_" + + # Ensure at least MAX_POLLING_INTERVAL seconds delay + self._refresh_interval = max(MAX_POLLING_INTERVAL, refresh_interval) + self._fast_polling_count = MAX_FAST_POLLING_COUNT + self._polling_task = None + + def start_periodic_request(self): + """Start periodic data polling.""" + self._polling_task = self._hass.loop.create_task(self._periodic_request()) + + async def _periodic_request(self): + """Send periodic update requests.""" + await self.request_data() + + if self._fast_polling_count < MAX_FAST_POLLING_COUNT: + self._fast_polling_count += 1 + _LOGGER.debug("Periodic data request executed, now wait for 2 seconds") + await asyncio.sleep(2) + else: + _LOGGER.debug( + "Periodic data request executed, now wait for %s seconds", + self._refresh_interval, + ) + await asyncio.sleep(self._refresh_interval) + + _LOGGER.debug("Periodic data request rescheduled") + self._polling_task = self._hass.loop.create_task(self._periodic_request()) + + async def setup(self, loop=None): + """Initialize KebaHandler object.""" + await super().setup(loop) + + # Request initial values and extract serial number + await self.request_data() + if self.get_value("Serial") is not None: + self.device_name = f"keba_wallbox_{self.get_value('Serial')}" + return True + + return False + + def hass_callback(self, data): + """Handle component notification via callback.""" + + # Inform entities about updated values + for listener in self._update_listeners: + listener() + + _LOGGER.debug("Notifying %d listeners", len(self._update_listeners)) + + def _set_fast_polling(self): + _LOGGER.debug("Fast polling enabled") + self._fast_polling_count = 0 + self._polling_task.cancel() + self._polling_task = self._hass.loop.create_task(self._periodic_request()) + + def add_update_listener(self, listener): + """Add a listener for update notifications.""" + self._update_listeners.append(listener) + + # initial data is already loaded, thus update the component + listener() + + async def async_set_energy(self, param): + """Set energy target in async way.""" + try: + energy = param["energy"] + await self.set_energy(energy) + self._set_fast_polling() + except (KeyError, ValueError) as ex: + _LOGGER.warning("Energy value is not correct. %s", ex) + + async def async_set_current(self, param): + """Set current maximum in async way.""" + try: + current = param["current"] + await self.set_current(current) + # No fast polling as this function might be called regularly + except (KeyError, ValueError) as ex: + _LOGGER.warning("Current value is not correct. %s", ex) + + async def async_start(self, param=None): + """Authorize EV in async way.""" + await self.start(self.rfid) + self._set_fast_polling() + + async def async_stop(self, param=None): + """De-authorize EV in async way.""" + await self.stop(self.rfid) + self._set_fast_polling() + + async def async_enable_ev(self, param=None): + """Enable EV in async way.""" + await self.enable(True) + self._set_fast_polling() + + async def async_disable_ev(self, param=None): + """Disable EV in async way.""" + await self.enable(False) + self._set_fast_polling() + + async def async_set_failsafe(self, param=None): + """Set failsafe mode in async way.""" + try: + timout = param[CONF_FS_TIMEOUT] + fallback = param[CONF_FS_FALLBACK] + persist = param[CONF_FS_PERSIST] + await self.set_failsafe(timout, fallback, persist) + self._set_fast_polling() + except (KeyError, ValueError) as ex: + _LOGGER.warning( + "failsafe_timeout, failsafe_fallback and/or " + "failsafe_persist value are not correct. %s", + ex, + ) diff --git a/homeassistant/components/keba/binary_sensor.py b/homeassistant/components/keba/binary_sensor.py new file mode 100644 index 00000000000..8c0503a2020 --- /dev/null +++ b/homeassistant/components/keba/binary_sensor.py @@ -0,0 +1,108 @@ +"""Support for KEBA charging station binary sensors.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_PLUG, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_SAFETY, +) + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the KEBA charging station platform.""" + if discovery_info is None: + return + + keba = hass.data[DOMAIN] + + sensors = [ + KebaBinarySensor(keba, "Online", "Wallbox", DEVICE_CLASS_CONNECTIVITY), + KebaBinarySensor(keba, "Plug", "Plug", DEVICE_CLASS_PLUG), + KebaBinarySensor(keba, "State", "Charging state", DEVICE_CLASS_POWER), + KebaBinarySensor(keba, "Tmo FS", "Failsafe Mode", DEVICE_CLASS_SAFETY), + ] + async_add_entities(sensors) + + +class KebaBinarySensor(BinarySensorDevice): + """Representation of a binary sensor of a KEBA charging station.""" + + def __init__(self, keba, key, sensor_name, device_class): + """Initialize the KEBA Sensor.""" + self._key = key + self._keba = keba + self._name = sensor_name + self._device_class = device_class + self._is_on = None + self._attributes = {} + + @property + def should_poll(self): + """Deactivate polling. Data updated by KebaHandler.""" + return False + + @property + def unique_id(self): + """Return the unique ID of the binary sensor.""" + return f"{self._keba.device_name}_{self._name}" + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._device_class + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._is_on + + @property + def device_state_attributes(self): + """Return the state attributes of the binary sensor.""" + return self._attributes + + async def async_update(self): + """Get latest cached states from the device.""" + if self._key == "Online": + self._is_on = self._keba.get_value(self._key) + + elif self._key == "Plug": + self._is_on = self._keba.get_value("Plug_plugged") + self._attributes["plugged_on_wallbox"] = self._keba.get_value( + "Plug_wallbox" + ) + self._attributes["plug_locked"] = self._keba.get_value("Plug_locked") + self._attributes["plugged_on_EV"] = self._keba.get_value("Plug_EV") + + elif self._key == "State": + self._is_on = self._keba.get_value("State_on") + self._attributes["status"] = self._keba.get_value("State_details") + self._attributes["max_charging_rate"] = str( + self._keba.get_value("Max curr") + ) + + elif self._key == "Tmo FS": + self._is_on = not self._keba.get_value("FS_on") + self._attributes["failsafe_timeout"] = str(self._keba.get_value("Tmo FS")) + self._attributes["fallback_current"] = str(self._keba.get_value("Curr FS")) + elif self._key == "Authreq": + self._is_on = self._keba.get_value(self._key) == 0 + + def update_callback(self): + """Schedule a state update.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Add update callback after being added to hass.""" + self._keba.add_update_listener(self.update_callback) diff --git a/homeassistant/components/keba/lock.py b/homeassistant/components/keba/lock.py new file mode 100644 index 00000000000..3a65e44cd6f --- /dev/null +++ b/homeassistant/components/keba/lock.py @@ -0,0 +1,69 @@ +"""Support for KEBA charging station switch.""" +import logging + +from homeassistant.components.lock import LockDevice + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the KEBA charging station platform.""" + if discovery_info is None: + return + + keba = hass.data[DOMAIN] + + sensors = [KebaLock(keba, "Authentication")] + async_add_entities(sensors) + + +class KebaLock(LockDevice): + """The entity class for KEBA charging stations switch.""" + + def __init__(self, keba, name): + """Initialize the KEBA switch.""" + self._keba = keba + self._name = name + self._state = True + + @property + def should_poll(self): + """Deactivate polling. Data updated by KebaHandler.""" + return False + + @property + def unique_id(self): + """Return the unique ID of the binary sensor.""" + return f"{self._keba.device_name}_{self._name}" + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def is_locked(self): + """Return true if lock is locked.""" + return self._state + + async def async_lock(self, **kwargs): + """Lock wallbox.""" + await self._keba.async_stop() + + async def async_unlock(self, **kwargs): + """Unlock wallbox.""" + await self._keba.async_start() + + async def async_update(self): + """Attempt to retrieve on off state from the switch.""" + self._state = self._keba.get_value("Authreq") == 1 + + def update_callback(self): + """Schedule a state update.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Add update callback after being added to hass.""" + self._keba.add_update_listener(self.update_callback) diff --git a/homeassistant/components/keba/manifest.json b/homeassistant/components/keba/manifest.json new file mode 100644 index 00000000000..9e959f35c9f --- /dev/null +++ b/homeassistant/components/keba/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "keba", + "name": "Keba Charging Station", + "documentation": "https://www.home-assistant.io/components/keba", + "requirements": ["keba-kecontact==0.2.0"], + "dependencies": [], + "codeowners": [ + "@dannerph" + ] +} diff --git a/homeassistant/components/keba/sensor.py b/homeassistant/components/keba/sensor.py new file mode 100644 index 00000000000..f46b2f0cf90 --- /dev/null +++ b/homeassistant/components/keba/sensor.py @@ -0,0 +1,109 @@ +"""Support for KEBA charging station sensors.""" +import logging + +from homeassistant.const import ENERGY_KILO_WATT_HOUR +from homeassistant.helpers.entity import Entity +from homeassistant.const import DEVICE_CLASS_POWER + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the KEBA charging station platform.""" + if discovery_info is None: + return + + keba = hass.data[DOMAIN] + + sensors = [ + KebaSensor(keba, "Curr user", "Max current", "mdi:flash", "A"), + KebaSensor( + keba, "Setenergy", "Energy target", "mdi:gauge", ENERGY_KILO_WATT_HOUR + ), + KebaSensor(keba, "P", "Charging power", "mdi:flash", "kW", DEVICE_CLASS_POWER), + KebaSensor( + keba, "E pres", "Session energy", "mdi:gauge", ENERGY_KILO_WATT_HOUR + ), + KebaSensor(keba, "E total", "Total Energy", "mdi:gauge", ENERGY_KILO_WATT_HOUR), + ] + async_add_entities(sensors) + + +class KebaSensor(Entity): + """The entity class for KEBA charging stations sensors.""" + + def __init__(self, keba, key, name, icon, unit, device_class=None): + """Initialize the KEBA Sensor.""" + self._key = key + self._keba = keba + self._name = name + self._device_class = device_class + self._icon = icon + self._unit = unit + self._state = None + self._attributes = {} + + @property + def should_poll(self): + """Deactivate polling. Data updated by KebaHandler.""" + return False + + @property + def unique_id(self): + """Return the unique ID of the binary sensor.""" + return f"{self._keba.device_name}_{self._name}" + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._device_class + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Get the unit of measurement.""" + return self._unit + + @property + def device_state_attributes(self): + """Return the state attributes of the binary sensor.""" + return self._attributes + + async def async_update(self): + """Get latest cached states from the device.""" + self._state = self._keba.get_value(self._key) + + if self._key == "P": + self._attributes["power_factor"] = self._keba.get_value("PF") + self._attributes["voltage_u1"] = str(self._keba.get_value("U1")) + self._attributes["voltage_u2"] = str(self._keba.get_value("U2")) + self._attributes["voltage_u3"] = str(self._keba.get_value("U3")) + self._attributes["current_i1"] = str(self._keba.get_value("I1")) + self._attributes["current_i2"] = str(self._keba.get_value("I2")) + self._attributes["current_i3"] = str(self._keba.get_value("I3")) + elif self._key == "Curr user": + self._attributes["max_current_hardware"] = self._keba.get_value("Curr HW") + + def update_callback(self): + """Schedule a state update.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Add update callback after being added to hass.""" + self._keba.add_update_listener(self.update_callback) diff --git a/homeassistant/components/keba/services.yaml b/homeassistant/components/keba/services.yaml new file mode 100644 index 00000000000..3422d6cf034 --- /dev/null +++ b/homeassistant/components/keba/services.yaml @@ -0,0 +1,56 @@ +# Describes the format for available services for KEBA charging staitons + +request_data: + description: > + Request new data from the charging station. + +authorize: + description: > + Authorizes a charging process with the predefined RFID tag of the configuration file. + +deauthorize: + description: > + Deauthorizes the running charging process with the predefined RFID tag of the configuration file. + +set_energy: + description: Sets the energy target after which the charging process stops. + fields: + energy: + description: > + The energy target to stop charging in kWh. Setting 0 disables the limit. + example: 10.0 + +set_current: + description: Sets the maximum current for charging processes. + fields: + current: + description: > + The maximum current used for the charging process in A. Allowed are values between + 6 A and 63 A. Invalid values are discardedand the default is set to 6 A. + The value is also depending on the DIP-switchsettings and the used cable of the + charging station + example: 16 +enable: + description: > + Starts a charging process if charging station is authorized. + +disable: + description: > + Stops the charging process if charging station is authorized. + +set_failsafe: + description: > + Set the failsafe mode of the charging station. If all parameters are 0, the failsafe mode will be disabled. + fields: + failsafe_timeout: + description: > + Timeout in seconds after which the failsafe mode is triggered, if set_current was not executed during this time. + example: 30 + failsafe_fallback: + description: > + Fallback current in A to be set after timeout. + example: 6 + failsafe_persist: + description: > + If failsafe_persist is 0, the failsafe option is only until charging station reboot. If failsafe_persist is 1, the failsafe option will survive a reboot. + example: 0 diff --git a/requirements_all.txt b/requirements_all.txt index c7b02188df6..cdf3a781883 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -692,6 +692,9 @@ jsonrpc-async==0.6 # homeassistant.components.kodi jsonrpc-websocket==0.6 +# homeassistant.components.keba +keba-kecontact==0.2.0 + # homeassistant.scripts.keyring keyring==17.1.1 From 1077ec17043e8364213e16bfc5bb6187d7590943 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 19 Aug 2019 16:56:57 +0200 Subject: [PATCH 197/273] Add packages version to Tensoflow for wheels packages (#26068) * Add packages version to Tensoflow for wheels packages * Uncomment tensorflow * fix string * Revert version --- homeassistant/components/tensorflow/manifest.json | 3 ++- requirements_all.txt | 3 +++ script/gen_requirements_all.py | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 9cbd349addc..f5bd981bad1 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -3,10 +3,11 @@ "name": "Tensorflow", "documentation": "https://www.home-assistant.io/components/tensorflow", "requirements": [ + "tensorflow==1.13.2", "numpy==1.17.0", "pillow==6.1.0", "protobuf==3.6.1" ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index cdf3a781883..5017e01fb95 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1820,6 +1820,9 @@ temescal==0.1 # homeassistant.components.temper temperusb==1.5.3 +# homeassistant.components.tensorflow +# tensorflow==1.13.2 + # homeassistant.components.tesla teslajsonpy==0.0.25 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 9417b926423..909be48352d 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -38,6 +38,7 @@ COMMENT_REQUIREMENTS = ( "rpi-rf", "RPi.GPIO", "smbus-cffi", + "tensorflow", ) TEST_REQUIREMENTS = ( From a38bdc4deb825fa673bad8114583ca20e46d59e5 Mon Sep 17 00:00:00 2001 From: Jonathan Keljo Date: Mon, 19 Aug 2019 10:08:07 -0700 Subject: [PATCH 198/273] Remove reference to typing.Deque (added in Python 3.6.1) (#26030) * Remove reference to typing.Deque (added in Python 3.6.1) * Silence mypy * Type as collections.deque --- homeassistant/components/camera/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 1b2bfb5fdb1..597d67fcdee 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -7,7 +7,6 @@ from datetime import timedelta import logging import hashlib from random import SystemRandom -from typing import Deque import attr from aiohttp import web @@ -315,7 +314,7 @@ class Camera(Entity): """Initialize a camera.""" self.is_streaming = False self.content_type = DEFAULT_CONTENT_TYPE - self.access_tokens: Deque[str] = collections.deque([], 2) + self.access_tokens: collections.deque = collections.deque([], 2) self.async_update_token() @property From 45aec2ea40c45ce74f87e0988e7659cfc316abc4 Mon Sep 17 00:00:00 2001 From: flebourse Date: Mon, 19 Aug 2019 19:34:33 +0200 Subject: [PATCH 199/273] huawei_lte: support out of range values in default sensor formatter (#26052) * Update sensor.py Change regexp to cope with out of range values, ie "rssi" : ">=-51dBm". * Add tests for format_default * Fix black formatting --- homeassistant/components/huawei_lte/sensor.py | 4 +++- tests/components/huawei_lte/test_sensor.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 tests/components/huawei_lte/test_sensor.py diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 4ef88eb783e..da78dc7d8cf 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -137,7 +137,9 @@ def format_default(value): unit = None if value is not None: # Clean up value and infer unit, e.g. -71dBm, 15 dB - match = re.match(r"(?P.+?)\s*(?P[a-zA-Z]+)\s*$", str(value)) + match = re.match( + r"([>=<]*)(?P.+?)\s*(?P[a-zA-Z]+)\s*$", str(value) + ) if match: try: value = float(match.group("value")) diff --git a/tests/components/huawei_lte/test_sensor.py b/tests/components/huawei_lte/test_sensor.py new file mode 100644 index 00000000000..f9834349750 --- /dev/null +++ b/tests/components/huawei_lte/test_sensor.py @@ -0,0 +1,14 @@ +"""Huawei LTE sensor tests.""" + +import pytest + +from homeassistant.components.huawei_lte import sensor + + +@pytest.mark.parametrize( + ("value", "expected"), + (("-71 dBm", (-71, "dBm")), ("15dB", (15, "dB")), (">=-51dBm", (-51, "dBm"))), +) +def test_format_default(value, expected): + """Test that default formatter copes with expected values.""" + assert sensor.format_default(value) == expected From b867e3314b670acd90826c2a27cf844b6495fbec Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Mon, 19 Aug 2019 22:10:35 +0200 Subject: [PATCH 200/273] SMA simplify config (#25880) --- homeassistant/components/sma/sensor.py | 85 ++++++++++++++++++-------- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/sma/__init__.py | 1 + tests/components/sma/test_sensor.py | 51 ++++++++++++++++ 5 files changed, 116 insertions(+), 25 deletions(-) create mode 100644 tests/components/sma/__init__.py create mode 100644 tests/components/sma/test_sensor.py diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 8e6b94ef5f8..b2692a37059 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -19,6 +19,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval +from homeassistant.const import MINOR_VERSION, MAJOR_VERSION _LOGGER = logging.getLogger(__name__) @@ -30,6 +31,7 @@ CONF_SENSORS = "sensors" CONF_UNIT = "unit" GROUPS = ["user", "installer"] +OLD_CONFIG_DEPRECATED = MAJOR_VERSION > 0 or MINOR_VERSION > 98 def _check_sensor_schema(conf): @@ -41,16 +43,39 @@ def _check_sensor_schema(conf): except (ImportError, AttributeError): return conf - for name in conf[CONF_CUSTOM]: - valid.append(name) + customs = list(conf[CONF_CUSTOM].keys()) - for sname, attrs in conf[CONF_SENSORS].items(): - if sname not in valid: - raise vol.Invalid("{} does not exist".format(sname)) - for attr in attrs: - if attr in valid: - continue - raise vol.Invalid("{} does not exist [{}]".format(attr, sname)) + if isinstance(conf[CONF_SENSORS], dict): + msg = '"sensors" should be a simple list from 0.99' + if OLD_CONFIG_DEPRECATED: + raise vol.Invalid(msg) + _LOGGER.warning(msg) + valid.extend(customs) + + for sname, attrs in conf[CONF_SENSORS].items(): + if sname not in valid: + raise vol.Invalid("{} does not exist".format(sname)) + if attrs: + _LOGGER.warning( + "Attributes on sensors will be deprecated in 0.99. Start using only individual sensors: %s: %s", + sname, + ", ".join(attrs), + ) + for attr in attrs: + if attr in valid: + continue + raise vol.Invalid("{} does not exist [{}]".format(attr, sname)) + return conf + + # Sensors is a list (only option from from 0.99) + for sensor in conf[CONF_SENSORS]: + if sensor in customs: + _LOGGER.warning( + "All custom sensors will be added automatically, no need to include them in sensors: %s", + sensor, + ) + elif sensor not in valid: + raise vol.Invalid("{} does not exist".format(sensor)) return conf @@ -59,7 +84,7 @@ CUSTOM_SCHEMA = vol.Any( vol.Required(CONF_KEY): vol.All(cv.string, vol.Length(min=13, max=15)), vol.Required(CONF_UNIT): cv.string, vol.Optional(CONF_FACTOR, default=1): vol.Coerce(float), - vol.Optional(CONF_PATH): vol.All(cv.ensure_list, [str]), + vol.Optional(CONF_PATH): vol.All(cv.ensure_list, [cv.string]), } ) @@ -71,8 +96,9 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_GROUP, default=GROUPS[0]): vol.In(GROUPS), - vol.Optional(CONF_SENSORS, default={}): cv.schema_with_slug_keys( - cv.ensure_list + vol.Optional(CONF_SENSORS, default=[]): vol.Any( + cv.schema_with_slug_keys(cv.ensure_list), # will be deprecated + vol.All(cv.ensure_list, [str]), ), vol.Optional(CONF_CUSTOM, default={}): cv.schema_with_slug_keys( CUSTOM_SCHEMA @@ -104,20 +130,29 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Use all sensors by default config_sensors = config[CONF_SENSORS] - if not config_sensors: - config_sensors = {s.name: [] for s in sensor_def} - - # Prepare all HASS sensor entities hass_sensors = [] used_sensors = [] - for name, attr in config_sensors.items(): - sub_sensors = [sensor_def[s] for s in attr] - hass_sensors.append(SMAsensor(sensor_def[name], sub_sensors)) - used_sensors.append(name) - used_sensors.extend(attr) + + if isinstance(config_sensors, dict): # will be remove from 0.99 + if not config_sensors: # Use all sensors by default + config_sensors = {s.name: [] for s in sensor_def} + + # Prepare all HASS sensor entities + for name, attr in config_sensors.items(): + sub_sensors = [sensor_def[s] for s in attr] + hass_sensors.append(SMAsensor(sensor_def[name], sub_sensors)) + used_sensors.append(name) + used_sensors.extend(attr) + used_sensors = [sensor_def[s] for s in set(used_sensors)] + + if isinstance(config_sensors, list): + if not config_sensors: # Use all sensors by default + config_sensors = [s.name for s in sensor_def] + used_sensors = list(set(config_sensors + list(config[CONF_CUSTOM].keys()))) + for sensor in used_sensors: + hass_sensors.append(SMAsensor(sensor_def[sensor], [])) async_add_entities(hass_sensors) - used_sensors = [sensor_def[s] for s in set(used_sensors)] # Init the SMA interface session = async_get_clientsession(hass, verify_ssl=config[CONF_VERIFY_SSL]) @@ -172,7 +207,7 @@ class SMAsensor(Entity): def __init__(self, pysma_sensor, sub_sensors): """Initialize the sensor.""" self._sensor = pysma_sensor - self._sub_sensors = sub_sensors + self._sub_sensors = sub_sensors # Can be remove from 0.99 self._attr = {s.name: "" for s in sub_sensors} self._state = self._sensor.value @@ -193,7 +228,7 @@ class SMAsensor(Entity): return self._sensor.unit @property - def device_state_attributes(self): + def device_state_attributes(self): # Can be remove from 0.99 """Return the state attributes of the sensor.""" return self._attr @@ -206,7 +241,7 @@ class SMAsensor(Entity): """Update this sensor.""" update = False - for sens in self._sub_sensors: + for sens in self._sub_sensors: # Can be remove from 0.99 newval = "{} {}".format(sens.value, sens.unit) if self._attr[sens.name] != newval: update = True diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a81c3ef290..a091027db74 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -309,6 +309,9 @@ pyps4-homeassistant==0.8.7 # homeassistant.components.qwikswitch pyqwikswitch==0.93 +# homeassistant.components.sma +pysma==0.3.2 + # homeassistant.components.smartthings pysmartapp==0.3.2 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 909be48352d..bcf645034f5 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -123,6 +123,7 @@ TEST_REQUIREMENTS = ( "pyopenuv", "pyotp", "pyps4-homeassistant", + "pysma", "pysmartapp", "pysmartthings", "pysonos", diff --git a/tests/components/sma/__init__.py b/tests/components/sma/__init__.py new file mode 100644 index 00000000000..124f481135e --- /dev/null +++ b/tests/components/sma/__init__.py @@ -0,0 +1 @@ +"""SMA tests.""" diff --git a/tests/components/sma/test_sensor.py b/tests/components/sma/test_sensor.py new file mode 100644 index 00000000000..bee1743791c --- /dev/null +++ b/tests/components/sma/test_sensor.py @@ -0,0 +1,51 @@ +"""SMA sensor tests.""" +import logging + +from homeassistant.components.sensor import DOMAIN +from homeassistant.setup import async_setup_component + +from tests.common import assert_setup_component + +_LOGGER = logging.getLogger(__name__) +BASE_CFG = { + "platform": "sma", + "host": "1.1.1.1", + "password": "", + "custom": {"my_sensor": {"key": "1234567890123", "unit": "V"}}, +} + + +async def test_sma_config_old(hass): + """Test old config.""" + sensors = {"current_consumption": ["current_consumption"]} + + with assert_setup_component(1): + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: dict(BASE_CFG, sensors=sensors)} + ) + + state = hass.states.get("sensor.current_consumption") + assert state + assert "unit_of_measurement" in state.attributes + assert "current_consumption" in state.attributes + + state = hass.states.get("sensor.my_sensor") + assert not state + + +async def test_sma_config(hass): + """Test new config.""" + sensors = ["current_consumption"] + + with assert_setup_component(1): + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: dict(BASE_CFG, sensors=sensors)} + ) + + state = hass.states.get("sensor.current_consumption") + assert state + assert "unit_of_measurement" in state.attributes + assert "current_consumption" not in state.attributes + + state = hass.states.get("sensor.my_sensor") + assert state From e41c002f7091bcd0e8f4c22ed9b224e896a0ef2e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 19 Aug 2019 23:42:21 +0200 Subject: [PATCH 201/273] Add config entry options support to deCONZ (#26049) --- .../components/deconz/.translations/en.json | 13 ++++- .../components/deconz/config_flow.py | 53 ++++++++++++++++++- homeassistant/components/deconz/strings.json | 11 ++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index dd8f1cc4026..34da602a6ce 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -40,5 +40,16 @@ } }, "title": "deCONZ Zigbee gateway" + }, + "options": { + "step": { + "deconz_devices": { + "description": "Configure visibility of deCONZ device types", + "data": { + "allow_clip_sensor": "Allow deCONZ CLIP sensors", + "allow_deconz_groups": "Allow deCONZ light groups" + } + } + } } -} \ No newline at end of file +} diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index f4b8d3ebe02..650c0285750 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -1,5 +1,6 @@ """Config flow to configure deCONZ component.""" import asyncio +from copy import copy import async_timeout import voluptuous as vol @@ -12,7 +13,13 @@ 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 +from .const import ( + CONF_ALLOW_CLIP_SENSOR, + CONF_ALLOW_DECONZ_GROUPS, + CONF_BRIDGEID, + DEFAULT_PORT, + DOMAIN, +) DECONZ_MANUFACTURERURL = "http://www.dresden-elektronik.de" CONF_SERIAL = "serial" @@ -45,6 +52,12 @@ class DeconzFlowHandler(config_entries.ConfigFlow): _hassio_discovery = None + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return DeconzOptionsFlowHandler(config_entry) + def __init__(self): """Initialize the deCONZ config flow.""" self.bridges = [] @@ -234,3 +247,41 @@ class DeconzFlowHandler(config_entries.ConfigFlow): step_id="hassio_confirm", description_placeholders={"addon": self._hassio_discovery["addon"]}, ) + + +class DeconzOptionsFlowHandler(config_entries.OptionsFlow): + """Handle deCONZ options.""" + + def __init__(self, config_entry): + """Initialize deCONZ options flow.""" + self.config_entry = config_entry + self.options = copy(config_entry.options) + + async def async_step_init(self, user_input=None): + """Manage the deCONZ options.""" + return await self.async_step_deconz_devices() + + async def async_step_deconz_devices(self, user_input=None): + """Manage the deconz devices options.""" + if user_input is not None: + self.options[CONF_ALLOW_CLIP_SENSOR] = user_input[CONF_ALLOW_CLIP_SENSOR] + self.options[CONF_ALLOW_DECONZ_GROUPS] = user_input[ + CONF_ALLOW_DECONZ_GROUPS + ] + return self.async_create_entry(title="", data=self.options) + + return self.async_show_form( + step_id="deconz_devices", + data_schema=vol.Schema( + { + vol.Optional( + CONF_ALLOW_CLIP_SENSOR, + default=self.config_entry.options[CONF_ALLOW_CLIP_SENSOR], + ): bool, + vol.Optional( + CONF_ALLOW_DECONZ_GROUPS, + default=self.config_entry.options[CONF_ALLOW_DECONZ_GROUPS], + ): bool, + } + ), + ) diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index d1c70793063..ea9ea280515 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -40,5 +40,16 @@ "one_instance_only": "Component only supports one deCONZ instance", "updated_instance": "Updated deCONZ instance with new host address" } + }, + "options": { + "step": { + "async_step_deconz_devices": { + "description": "Configure visibility of deCONZ device types", + "data": { + "allow_clip_sensor": "Allow deCONZ CLIP sensors", + "allow_deconz_groups": "Allow deCONZ light groups" + } + } + } } } From d1483b6f29501159c780ffb74aa415c31cb1b810 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Tue, 20 Aug 2019 01:44:19 +0200 Subject: [PATCH 202/273] pysma library update 0.3.4 (#26075) --- homeassistant/components/sma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 8795029bff2..ea3a33d55ff 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "Sma", "documentation": "https://www.home-assistant.io/components/sma", "requirements": [ - "pysma==0.3.2" + "pysma==0.3.4" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 5017e01fb95..8e9cf5d7147 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1386,7 +1386,7 @@ pysesame2==1.0.1 pysher==1.0.1 # homeassistant.components.sma -pysma==0.3.2 +pysma==0.3.4 # homeassistant.components.smartthings pysmartapp==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a091027db74..6d8feb7b984 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -310,7 +310,7 @@ pyps4-homeassistant==0.8.7 pyqwikswitch==0.93 # homeassistant.components.sma -pysma==0.3.2 +pysma==0.3.4 # homeassistant.components.smartthings pysmartapp==0.3.2 From a1dbdbba6a2647c96e44e17a0192c7a3210a9323 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 19 Aug 2019 16:45:17 -0700 Subject: [PATCH 203/273] Save config entry after updating system options (#26077) --- homeassistant/components/config/config_entries.py | 10 +++++++--- homeassistant/config_entries.py | 14 +++++++++----- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index d7c8a6ea8e0..90ae92cae84 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -272,7 +272,11 @@ async def system_options_update(hass, connection, msg): entry_id = changes.pop("entry_id") entry = hass.config_entries.async_get_entry(entry_id) - if entry and changes: - entry.system_options.update(**changes) + if entry is None: + connection.send_error( + msg["id"], websocket_api.const.ERR_NOT_FOUND, "Config entry not found" + ) + return - connection.send_result(msg["id"], entry.system_options.as_dict()) + hass.config_entries.async_update_entry(entry, system_options=changes) + connection.send_result(msg["id"], entry.system_options.as_dict()) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 9844aeb9ca6..87bce1a870c 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -522,7 +522,9 @@ class ConfigEntries: return await self.async_setup(entry_id) @callback - def async_update_entry(self, entry, *, data=_UNDEF, options=_UNDEF): + def async_update_entry( + self, entry, *, data=_UNDEF, options=_UNDEF, system_options=_UNDEF + ): """Update a config entry.""" if data is not _UNDEF: entry.data = data @@ -530,10 +532,12 @@ class ConfigEntries: if options is not _UNDEF: entry.options = options - if data is not _UNDEF or options is not _UNDEF: - for listener_ref in entry.update_listeners: - listener = listener_ref() - self.hass.async_create_task(listener(self.hass, entry)) + if system_options is not _UNDEF: + entry.system_options.update(**system_options) + + for listener_ref in entry.update_listeners: + listener = listener_ref() + self.hass.async_create_task(listener(self.hass, entry)) self._async_schedule_save() From 1fe3b147fa2993b6aee763e0fd98f22c431ae2ff Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Aug 2019 00:15:53 -0700 Subject: [PATCH 204/273] Updated frontend to 20190820.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 2ccda8a1f4a..f7c1a4f874f 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==20190815.0" + "home-assistant-frontend==20190820.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1abb8d1d822..74ffb83f884 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.17 -home-assistant-frontend==20190815.0 +home-assistant-frontend==20190820.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 8e9cf5d7147..9c578e45b1c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -627,7 +627,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190815.0 +home-assistant-frontend==20190820.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d8feb7b984..7b757d8e1c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -177,7 +177,7 @@ hdate==0.9.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190815.0 +home-assistant-frontend==20190820.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 93a800a6121f32eb2af9b64e93b1fd0117c81785 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Aug 2019 00:17:52 -0700 Subject: [PATCH 205/273] Convert progress API to WS (#26082) --- .../components/config/config_entries.py | 39 +++++++++++-------- .../components/config/test_config_entries.py | 36 ++++++++++------- 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 90ae92cae84..b21991a8479 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -1,4 +1,5 @@ """Http views to control the config manager.""" +import aiohttp.web_exceptions import voluptuous as vol from homeassistant import config_entries, data_entry_flow @@ -28,6 +29,7 @@ async def async_setup(hass): OptionManagerFlowResourceView(hass.config_entries.options.flow) ) + hass.components.websocket_api.async_register_command(config_entries_progress) hass.components.websocket_api.async_register_command(system_options_list) hass.components.websocket_api.async_register_command(system_options_update) @@ -116,23 +118,8 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView): name = "api:config:config_entries:flow" async def get(self, request): - """List flows that are in progress but not started by a user. - - Example of a non-user initiated flow is a discovered Hue hub that - requires user interaction to finish setup. - """ - if not request["hass_user"].is_admin: - raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add") - - hass = request.app["hass"] - - return self.json( - [ - flw - for flw in hass.config_entries.flow.async_progress() - if flw["context"]["source"] != config_entries.SOURCE_USER - ] - ) + """Not implemented.""" + raise aiohttp.web_exceptions.HTTPMethodNotAllowed("GET", ["POST"]) # pylint: disable=arguments-differ async def post(self, request): @@ -241,6 +228,24 @@ class OptionManagerFlowResourceView(FlowManagerResourceView): return await super().post(request, flow_id) +@websocket_api.require_admin +@websocket_api.websocket_command({"type": "config_entries/flow/progress"}) +def config_entries_progress(hass, connection, msg): + """List flows that are in progress but not started by a user. + + Example of a non-user initiated flow is a discovered Hue hub that + requires user interaction to finish setup. + """ + connection.send_result( + msg["id"], + [ + flw + for flw in hass.config_entries.flow.async_progress() + if flw["context"]["source"] != config_entries.SOURCE_USER + ], + ) + + @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index f0815e7ede8..3d22d3ac1a7 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -373,40 +373,46 @@ async def test_continue_flow_unauth(hass, client, hass_admin_user): assert resp.status == 401 -@asyncio.coroutine -def test_get_progress_index(hass, client): +async def test_get_progress_index(hass, hass_ws_client): """Test querying for the flows that are in progress.""" + assert await async_setup_component(hass, "config", {}) mock_entity_platform(hass, "config_flow.test", None) + ws_client = await hass_ws_client(hass) class TestFlow(core_ce.ConfigFlow): VERSION = 5 - @asyncio.coroutine - def async_step_hassio(self, info): - return (yield from self.async_step_account()) + async def async_step_hassio(self, info): + return await self.async_step_account() - @asyncio.coroutine - def async_step_account(self, user_input=None): + async def async_step_account(self, user_input=None): return self.async_show_form(step_id="account") with patch.dict(HANDLERS, {"test": TestFlow}): - form = yield from hass.config_entries.flow.async_init( + form = await hass.config_entries.flow.async_init( "test", context={"source": "hassio"} ) - resp = yield from client.get("/api/config/config_entries/flow") - assert resp.status == 200 - data = yield from resp.json() - assert data == [ + await ws_client.send_json({"id": 5, "type": "config_entries/flow/progress"}) + response = await ws_client.receive_json() + + assert response["success"] + assert response["result"] == [ {"flow_id": form["flow_id"], "handler": "test", "context": {"source": "hassio"}} ] -async def test_get_progress_index_unauth(hass, client, hass_admin_user): +async def test_get_progress_index_unauth(hass, hass_ws_client, hass_admin_user): """Test we can't get flows that are in progress.""" + assert await async_setup_component(hass, "config", {}) hass_admin_user.groups = [] - resp = await client.get("/api/config/config_entries/flow") - assert resp.status == 401 + ws_client = await hass_ws_client(hass) + + await ws_client.send_json({"id": 5, "type": "config_entries/flow/progress"}) + response = await ws_client.receive_json() + + assert not response["success"] + assert response["error"]["code"] == "unauthorized" @asyncio.coroutine From eee2b2d54346fb83026ac3b590981edd06a60f5d Mon Sep 17 00:00:00 2001 From: Tomas Kislan Date: Tue, 20 Aug 2019 11:56:11 +0200 Subject: [PATCH 206/273] Add Minio component (#23567) * Add minio implementation * Static check changes * Added docstrings * Update docstrings * Update docstrings * Fix linter errors * Finally fix all docstring errors * Create services.yaml * Update CODEOWNERS * Final changes * Remove double underscores * Minor changes * Update config.yml * Review changes * Added tests * Fix lint errors * Move tests from unittest to pytest * Add minio as test requirement * Update test_minio_helper.py * Better event thread handling, added hass test * Update tests * Fixed lint errors * Update test_minio.py * Review changes * More review changes * Removed tests * Applied code style changes * Reformat test code --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/minio/__init__.py | 265 ++++++++++++++++++ homeassistant/components/minio/manifest.json | 12 + .../components/minio/minio_helper.py | 209 ++++++++++++++ homeassistant/components/minio/services.yaml | 35 +++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/minio/__init__.py | 1 + tests/components/minio/common.py | 48 ++++ tests/components/minio/test_minio.py | 190 +++++++++++++ 12 files changed, 769 insertions(+) create mode 100644 homeassistant/components/minio/__init__.py create mode 100644 homeassistant/components/minio/manifest.json create mode 100644 homeassistant/components/minio/minio_helper.py create mode 100644 homeassistant/components/minio/services.yaml create mode 100644 tests/components/minio/__init__.py create mode 100644 tests/components/minio/common.py create mode 100644 tests/components/minio/test_minio.py diff --git a/.coveragerc b/.coveragerc index d8153a7635c..e0bbbd66d7c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -378,6 +378,7 @@ omit = homeassistant/components/miflora/sensor.py homeassistant/components/mikrotik/* homeassistant/components/mill/climate.py + homeassistant/components/minio/* homeassistant/components/mitemp_bt/sensor.py homeassistant/components/mjpeg/camera.py homeassistant/components/mobile_app/* diff --git a/CODEOWNERS b/CODEOWNERS index f6b9e79b8bd..1425c476478 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -173,6 +173,7 @@ homeassistant/components/meteoalarm/* @rolfberkenbosch homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel homeassistant/components/mill/* @danielhiversen homeassistant/components/min_max/* @fabaff +homeassistant/components/minio/* @tkislan homeassistant/components/mobile_app/* @robbiet480 homeassistant/components/monoprice/* @etsinko homeassistant/components/moon/* @fabaff diff --git a/homeassistant/components/minio/__init__.py b/homeassistant/components/minio/__init__.py new file mode 100644 index 00000000000..cede3a7aad5 --- /dev/null +++ b/homeassistant/components/minio/__init__.py @@ -0,0 +1,265 @@ +"""Minio component.""" +import logging +import os +import threading +from queue import Queue +from typing import List + +import voluptuous as vol + +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +import homeassistant.helpers.config_validation as cv + +from .minio_helper import create_minio_client, MinioEventThread + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "minio" +CONF_HOST = "host" +CONF_PORT = "port" +CONF_ACCESS_KEY = "access_key" +CONF_SECRET_KEY = "secret_key" +CONF_SECURE = "secure" +CONF_LISTEN = "listen" +CONF_LISTEN_BUCKET = "bucket" +CONF_LISTEN_PREFIX = "prefix" +CONF_LISTEN_SUFFIX = "suffix" +CONF_LISTEN_EVENTS = "events" + +ATTR_BUCKET = "bucket" +ATTR_KEY = "key" +ATTR_FILE_PATH = "file_path" + +DEFAULT_LISTEN_PREFIX = "" +DEFAULT_LISTEN_SUFFIX = ".*" +DEFAULT_LISTEN_EVENTS = "s3:ObjectCreated:*" + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Required(CONF_ACCESS_KEY): cv.string, + vol.Required(CONF_SECRET_KEY): cv.string, + vol.Required(CONF_SECURE): cv.boolean, + vol.Optional(CONF_LISTEN, default=[]): vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Required(CONF_LISTEN_BUCKET): cv.string, + vol.Optional( + CONF_LISTEN_PREFIX, default=DEFAULT_LISTEN_PREFIX + ): cv.string, + vol.Optional( + CONF_LISTEN_SUFFIX, default=DEFAULT_LISTEN_SUFFIX + ): cv.string, + vol.Optional( + CONF_LISTEN_EVENTS, default=DEFAULT_LISTEN_EVENTS + ): cv.string, + } + ) + ], + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + +BUCKET_KEY_SCHEMA = vol.Schema( + {vol.Required(ATTR_BUCKET): cv.template, vol.Required(ATTR_KEY): cv.template} +) + +BUCKET_KEY_FILE_SCHEMA = BUCKET_KEY_SCHEMA.extend( + {vol.Required(ATTR_FILE_PATH): cv.template} +) + + +def setup(hass, config): + """Set up MinioClient and event listeners.""" + conf = config[DOMAIN] + + host = conf[CONF_HOST] + port = conf[CONF_PORT] + access_key = conf[CONF_ACCESS_KEY] + secret_key = conf[CONF_SECRET_KEY] + secure = conf[CONF_SECURE] + + queue_listener = QueueListener(hass) + queue = queue_listener.queue + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, queue_listener.start_handler) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, queue_listener.stop_handler) + + def _setup_listener(listener_conf): + bucket = listener_conf[CONF_LISTEN_BUCKET] + prefix = listener_conf[CONF_LISTEN_PREFIX] + suffix = listener_conf[CONF_LISTEN_SUFFIX] + events = listener_conf[CONF_LISTEN_EVENTS] + + minio_listener = MinioListener( + queue, + get_minio_endpoint(host, port), + access_key, + secret_key, + secure, + bucket, + prefix, + suffix, + events, + ) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, minio_listener.start_handler) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, minio_listener.stop_handler) + + for listen_conf in conf[CONF_LISTEN]: + _setup_listener(listen_conf) + + minio_client = create_minio_client( + get_minio_endpoint(host, port), access_key, secret_key, secure + ) + + def _render_service_value(service, key): + value = service.data[key] + value.hass = hass + return value.async_render() + + def put_file(service): + """Upload file service.""" + bucket = _render_service_value(service, ATTR_BUCKET) + key = _render_service_value(service, ATTR_KEY) + file_path = _render_service_value(service, ATTR_FILE_PATH) + + if not hass.config.is_allowed_path(file_path): + _LOGGER.error("Invalid file_path %s", file_path) + return + + minio_client.fput_object(bucket, key, file_path) + + def get_file(service): + """Download file service.""" + bucket = _render_service_value(service, ATTR_BUCKET) + key = _render_service_value(service, ATTR_KEY) + file_path = _render_service_value(service, ATTR_FILE_PATH) + + if not hass.config.is_allowed_path(file_path): + _LOGGER.error("Invalid file_path %s", file_path) + return + + minio_client.fget_object(bucket, key, file_path) + + def remove_file(service): + """Delete file service.""" + bucket = _render_service_value(service, ATTR_BUCKET) + key = _render_service_value(service, ATTR_KEY) + + minio_client.remove_object(bucket, key) + + hass.services.register(DOMAIN, "put", put_file, schema=BUCKET_KEY_FILE_SCHEMA) + hass.services.register(DOMAIN, "get", get_file, schema=BUCKET_KEY_FILE_SCHEMA) + hass.services.register(DOMAIN, "remove", remove_file, schema=BUCKET_KEY_SCHEMA) + + return True + + +def get_minio_endpoint(host: str, port: int) -> str: + """Create minio endpoint from host and port.""" + return "{}:{}".format(host, port) + + +class QueueListener(threading.Thread): + """Forward events from queue into HASS event bus.""" + + def __init__(self, hass): + """Create queue.""" + super().__init__() + self._hass = hass + self._queue = Queue() + + def run(self): + """Listen to queue events, and forward them to HASS event bus.""" + _LOGGER.info("Running QueueListener") + while True: + event = self._queue.get() + if event is None: + break + + _, file_name = os.path.split(event[ATTR_KEY]) + + _LOGGER.debug( + "Sending event %s, %s, %s", + event["event_name"], + event[ATTR_BUCKET], + event[ATTR_KEY], + ) + self._hass.bus.fire(DOMAIN, {"file_name": file_name, **event}) + + @property + def queue(self): + """Return wrapped queue.""" + return self._queue + + def stop(self): + """Stop run by putting None into queue and join the thread.""" + _LOGGER.info("Stopping QueueListener") + self._queue.put(None) + self.join() + _LOGGER.info("Stopped QueueListener") + + def start_handler(self, _): + """Start handler helper method.""" + self.start() + + def stop_handler(self, _): + """Stop handler helper method.""" + self.stop() + + +class MinioListener: + """MinioEventThread wrapper with helper methods.""" + + def __init__( + self, + queue: Queue, + endpoint: str, + access_key: str, + secret_key: str, + secure: bool, + bucket_name: str, + prefix: str, + suffix: str, + events: List[str], + ): + """Create Listener.""" + self._queue = queue + self._endpoint = endpoint + self._access_key = access_key + self._secret_key = secret_key + self._secure = secure + self._bucket_name = bucket_name + self._prefix = prefix + self._suffix = suffix + self._events = events + self._minio_event_thread = None + + def start_handler(self, _): + """Create and start the event thread.""" + self._minio_event_thread = MinioEventThread( + self._queue, + self._endpoint, + self._access_key, + self._secret_key, + self._secure, + self._bucket_name, + self._prefix, + self._suffix, + self._events, + ) + self._minio_event_thread.start() + + def stop_handler(self, _): + """Issue stop and wait for thread to join.""" + if self._minio_event_thread is not None: + self._minio_event_thread.stop() diff --git a/homeassistant/components/minio/manifest.json b/homeassistant/components/minio/manifest.json new file mode 100644 index 00000000000..2b2f84836ea --- /dev/null +++ b/homeassistant/components/minio/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "minio", + "name": "Minio", + "documentation": "https://www.home-assistant.io/components/minio", + "requirements": [ + "minio==4.0.9" + ], + "dependencies": [], + "codeowners": [ + "@tkislan" + ] +} diff --git a/homeassistant/components/minio/minio_helper.py b/homeassistant/components/minio/minio_helper.py new file mode 100644 index 00000000000..bd7b15d27d4 --- /dev/null +++ b/homeassistant/components/minio/minio_helper.py @@ -0,0 +1,209 @@ +"""Minio helper methods.""" +import time +from collections.abc import Iterable +import json +import logging +import re +import threading +from queue import Queue +from typing import Iterator, List +from urllib.parse import unquote + +from minio import Minio +from urllib3.exceptions import HTTPError + +_LOGGER = logging.getLogger(__name__) + +_METADATA_RE = re.compile("x-amz-meta-(.*)", re.IGNORECASE) + + +def normalize_metadata(metadata: dict) -> dict: + """Normalize object metadata by stripping the prefix.""" + new_metadata = {} + for meta_key, meta_value in metadata.items(): + match = _METADATA_RE.match(meta_key) + if not match: + continue + + new_metadata[match.group(1).lower()] = meta_value + + return new_metadata + + +def create_minio_client( + endpoint: str, access_key: str, secret_key: str, secure: bool +) -> Minio: + """Create Minio client.""" + return Minio(endpoint, access_key, secret_key, secure) + + +def get_minio_notification_response( + minio_client, bucket_name: str, prefix: str, suffix: str, events: List[str] +): + """Start listening to minio events. Copied from minio-py.""" + query = {"prefix": prefix, "suffix": suffix, "events": events} + # pylint: disable=protected-access + return minio_client._url_open( + "GET", bucket_name=bucket_name, query=query, preload_content=False + ) + + +class MinioEventStreamIterator(Iterable): + """Iterator wrapper over notification http response stream.""" + + def __iter__(self) -> Iterator: + """Return self.""" + return self + + def __init__(self, response): + """Init.""" + self._response = response + self._stream = response.stream() + + def __next__(self): + """Get next not empty line.""" + while True: + line = next(self._stream) + if line.strip(): + event = json.loads(line.decode("utf-8")) + if event["Records"] is not None: + return event + + def close(self): + """Close the response.""" + self._response.close() + + +class MinioEventThread(threading.Thread): + """Thread wrapper around minio notification blocking stream.""" + + def __init__( + self, + queue: Queue, + endpoint: str, + access_key: str, + secret_key: str, + secure: bool, + bucket_name: str, + prefix: str, + suffix: str, + events: List[str], + ): + """Copy over all Minio client options.""" + super().__init__() + self._queue = queue + self._endpoint = endpoint + self._access_key = access_key + self._secret_key = secret_key + self._secure = secure + self._bucket_name = bucket_name + self._prefix = prefix + self._suffix = suffix + self._events = events + self._event_stream_it = None + self._should_stop = False + + def __enter__(self): + """Start the thread.""" + self.start() + + def __exit__(self, exc_type, exc_val, exc_tb): + """Stop and join the thread.""" + self.stop() + + def run(self): + """Create MinioClient and run the loop.""" + _LOGGER.info("Running MinioEventThread") + + self._should_stop = False + + minio_client = create_minio_client( + self._endpoint, self._access_key, self._secret_key, self._secure + ) + + while not self._should_stop: + _LOGGER.info("Connecting to minio event stream") + response = None + try: + response = get_minio_notification_response( + minio_client, + self._bucket_name, + self._prefix, + self._suffix, + self._events, + ) + + self._event_stream_it = MinioEventStreamIterator(response) + + self._iterate_event_stream(self._event_stream_it, minio_client) + except json.JSONDecodeError: + if response: + response.close() + except HTTPError as error: + _LOGGER.error("Failed to connect to Minio endpoint: %s", error) + + # Wait before attempting to connect again. + time.sleep(1) + except AttributeError: + # When response is closed, iterator will fail to access + # the underlying socket descriptor. + break + + def _iterate_event_stream(self, event_stream_it, minio_client): + for event in event_stream_it: + for event_name, bucket, key, metadata in iterate_objects(event): + presigned_url = "" + try: + presigned_url = minio_client.presigned_get_object(bucket, key) + # Fail gracefully. If for whatever reason this stops working, + # it shouldn't prevent it from firing events. + # pylint: disable=broad-except + except Exception as error: + _LOGGER.error("Failed to generate presigned url: %s", error) + + queue_entry = { + "event_name": event_name, + "bucket": bucket, + "key": key, + "presigned_url": presigned_url, + "metadata": metadata, + } + _LOGGER.debug("Queue entry, %s", queue_entry) + self._queue.put(queue_entry) + + def stop(self): + """Cancel event stream and join the thread.""" + _LOGGER.debug("Stopping event thread") + self._should_stop = True + if self._event_stream_it is not None: + self._event_stream_it.close() + self._event_stream_it = None + + _LOGGER.debug("Joining event thread") + self.join() + _LOGGER.debug("Event thread joined") + + +def iterate_objects(event): + """ + Iterate over file records of notification event. + + Most of the time it should still be only one record. + """ + records = event.get("Records", []) + + for record in records: + event_name = record.get("eventName") + bucket = record.get("s3", {}).get("bucket", {}).get("name") + key = record.get("s3", {}).get("object", {}).get("key") + metadata = normalize_metadata( + record.get("s3", {}).get("object", {}).get("userMetadata", {}) + ) + + if not bucket or not key: + _LOGGER.warning("Invalid bucket and/or key, %s, %s", bucket, key) + continue + + key = unquote(key) + + yield event_name, bucket, key, metadata diff --git a/homeassistant/components/minio/services.yaml b/homeassistant/components/minio/services.yaml new file mode 100644 index 00000000000..8fb8a267c3b --- /dev/null +++ b/homeassistant/components/minio/services.yaml @@ -0,0 +1,35 @@ +get: + description: Download file from Minio. + fields: + bucket: + description: Bucket to use. + example: camera-files + key: + description: Object key of the file. + example: front_camera/2018/01/02/snapshot_12512514.jpg + file_path: + description: File path on local filesystem. + example: /data/camera_files/snapshot.jpg + +put: + description: Upload file to Minio. + fields: + bucket: + description: Bucket to use. + example: camera-files + key: + description: Object key of the file. + example: front_camera/2018/01/02/snapshot_12512514.jpg + file_path: + description: File path on local filesystem. + example: /data/camera_files/snapshot.jpg + +remove: + description: Delete file from Minio. + fields: + bucket: + description: Bucket to use. + example: camera-files + key: + description: Object key of the file. + example: front_camera/2018/01/02/snapshot_12512514.jpg diff --git a/requirements_all.txt b/requirements_all.txt index 9c578e45b1c..6cd7f5d2822 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -797,6 +797,9 @@ miflora==0.4.0 # homeassistant.components.mill millheater==0.3.4 +# homeassistant.components.minio +minio==4.0.9 + # homeassistant.components.mitemp_bt mitemp_bt==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b757d8e1c1..e2829d2018c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -213,6 +213,9 @@ mbddns==0.1.2 # homeassistant.components.mfi mficlient==0.3.0 +# homeassistant.components.minio +minio==4.0.9 + # homeassistant.components.discovery # homeassistant.components.ssdp netdisco==2.6.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index bcf645034f5..6643fcf7aa9 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -99,6 +99,7 @@ TEST_REQUIREMENTS = ( "pyMetno", "mbddns", "mficlient", + "minio", "netdisco", "numpy", "oauth2client", diff --git a/tests/components/minio/__init__.py b/tests/components/minio/__init__.py new file mode 100644 index 00000000000..273de09788e --- /dev/null +++ b/tests/components/minio/__init__.py @@ -0,0 +1 @@ +"""Tests for the minio component.""" diff --git a/tests/components/minio/common.py b/tests/components/minio/common.py new file mode 100644 index 00000000000..4719fc79e49 --- /dev/null +++ b/tests/components/minio/common.py @@ -0,0 +1,48 @@ +"""Minio Test event.""" +TEST_EVENT = { + "Records": [ + { + "eventVersion": "2.0", + "eventSource": "minio:s3", + "awsRegion": "", + "eventTime": "2019-05-02T11:05:07Z", + "eventName": "s3:ObjectCreated:Put", + "userIdentity": {"principalId": "SO9KNO6YT9OGE39PQCZW"}, + "requestParameters": { + "accessKey": "SO9KNO6YT9OGE39PQCZW", + "region": "", + "sourceIPAddress": "172.27.0.1", + }, + "responseElements": { + "x-amz-request-id": "159AD8E6F6805783", + "x-minio-deployment-id": "90b265b8-bac5-413a-b12a-8915469fd769", + "x-minio-origin-endpoint": "http://172.27.0.2:9000", + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "Config", + "bucket": { + "name": "test", + "ownerIdentity": {"principalId": "SO9KNO6YT9OGE39PQCZW"}, + "arn": "arn:aws:s3:::test", + }, + "object": { + "key": "5jJkTAo.jpg", + "size": 108368, + "eTag": "1af324731637228cbbb0b2e8c07d4e50", + "contentType": "image/jpeg", + "userMetadata": {"content-type": "image/jpeg"}, + "versionId": "1", + "sequencer": "159AD8E6F76DD9C4", + }, + }, + "source": { + "host": "", + "port": "", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) " + "AppleWebKit/605.1.15 (KHTML, like Gecko) " + "Version/12.0.3 Safari/605.1.15", + }, + } + ] +} diff --git a/tests/components/minio/test_minio.py b/tests/components/minio/test_minio.py new file mode 100644 index 00000000000..836b456dc9b --- /dev/null +++ b/tests/components/minio/test_minio.py @@ -0,0 +1,190 @@ +"""Tests for Minio Hass related code.""" +import asyncio +import json +from unittest.mock import MagicMock + +import pytest +from asynctest import patch, call + +from homeassistant.components.minio import ( + QueueListener, + DOMAIN, + CONF_HOST, + CONF_PORT, + CONF_ACCESS_KEY, + CONF_SECRET_KEY, + CONF_SECURE, + CONF_LISTEN, + CONF_LISTEN_BUCKET, +) +from homeassistant.core import callback +from homeassistant.setup import async_setup_component + +from tests.components.minio.common import TEST_EVENT + + +@pytest.fixture(name="minio_client") +def minio_client_fixture(): + """Patch Minio client.""" + with patch("homeassistant.components.minio.minio_helper.Minio") as minio_mock: + minio_client_mock = minio_mock.return_value + + yield minio_client_mock + + +@pytest.fixture(name="minio_client_event") +def minio_client_event_fixture(): + """Patch helper function for minio notification stream.""" + with patch("homeassistant.components.minio.minio_helper.Minio") as minio_mock: + minio_client_mock = minio_mock.return_value + + response_mock = MagicMock() + stream_mock = MagicMock() + + stream_mock.__next__.side_effect = [ + "", + "", + bytearray(json.dumps(TEST_EVENT), "utf-8"), + ] + + response_mock.stream.return_value = stream_mock + minio_client_mock._url_open.return_value = response_mock + + yield minio_client_mock + + +async def test_minio_services(hass, caplog, minio_client): + """Test Minio services.""" + hass.config.whitelist_external_dirs = set("/tmp") + + await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_HOST: "localhost", + CONF_PORT: "9000", + CONF_ACCESS_KEY: "abcdef", + CONF_SECRET_KEY: "0123456789", + CONF_SECURE: "true", + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert "Setup of domain minio took" in caplog.text + + # Call services + await hass.services.async_call( + DOMAIN, + "put", + {"file_path": "/tmp/some_file", "key": "some_key", "bucket": "some_bucket"}, + blocking=True, + ) + assert minio_client.fput_object.call_args == call( + "some_bucket", "some_key", "/tmp/some_file" + ) + minio_client.reset_mock() + + await hass.services.async_call( + DOMAIN, + "get", + {"file_path": "/tmp/some_file", "key": "some_key", "bucket": "some_bucket"}, + blocking=True, + ) + assert minio_client.fget_object.call_args == call( + "some_bucket", "some_key", "/tmp/some_file" + ) + minio_client.reset_mock() + + await hass.services.async_call( + DOMAIN, "remove", {"key": "some_key", "bucket": "some_bucket"}, blocking=True + ) + assert minio_client.remove_object.call_args == call("some_bucket", "some_key") + minio_client.reset_mock() + + +async def test_minio_listen(hass, caplog, minio_client_event): + """Test minio listen on notifications.""" + minio_client_event.presigned_get_object.return_value = "http://url" + + events = [] + + @callback + def event_callback(event): + """Handle event callbback.""" + events.append(event) + + hass.bus.async_listen("minio", event_callback) + + await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_HOST: "localhost", + CONF_PORT: "9000", + CONF_ACCESS_KEY: "abcdef", + CONF_SECRET_KEY: "0123456789", + CONF_SECURE: "true", + CONF_LISTEN: [{CONF_LISTEN_BUCKET: "test"}], + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert "Setup of domain minio took" in caplog.text + + while not events: + await asyncio.sleep(0) + + assert 1 == len(events) + event = events[0] + + assert DOMAIN == event.event_type + assert "s3:ObjectCreated:Put" == event.data["event_name"] + assert "5jJkTAo.jpg" == event.data["file_name"] + assert "test" == event.data["bucket"] + assert "5jJkTAo.jpg" == event.data["key"] + assert "http://url" == event.data["presigned_url"] + assert 0 == len(event.data["metadata"]) + + +async def test_queue_listener(): + """Tests QueueListener firing events on Hass event bus.""" + hass = MagicMock() + + queue_listener = QueueListener(hass) + queue_listener.start() + + queue_entry = { + "event_name": "s3:ObjectCreated:Put", + "bucket": "some_bucket", + "key": "some_dir/some_file.jpg", + "presigned_url": "http://host/url?signature=secret", + "metadata": {}, + } + + queue_listener.queue.put(queue_entry) + + queue_listener.stop() + + call_domain, call_event = hass.bus.fire.call_args[0] + + expected_event = { + "event_name": "s3:ObjectCreated:Put", + "file_name": "some_file.jpg", + "bucket": "some_bucket", + "key": "some_dir/some_file.jpg", + "presigned_url": "http://host/url?signature=secret", + "metadata": {}, + } + + assert DOMAIN == call_domain + assert json.dumps(expected_event, sort_keys=True) == json.dumps( + call_event, sort_keys=True + ) From c5ca43189402adb78709631f1dd37e48b020edd1 Mon Sep 17 00:00:00 2001 From: Chris Thornton <54046872+cj-thornton@users.noreply.github.com> Date: Tue, 20 Aug 2019 10:01:19 +0000 Subject: [PATCH 207/273] Add path option to SABnzbd component (#25908) * Add path option to SABnzbd component Adds an optional `path` setting to the SABnzbd component. This allows support for SABnzbd installs that use a different `url_base` (typically a reverse proxied configuration; see https://sabnzbd.org/wiki/configuration/2.3/special). This change passes the `path` along to pysabnzbd as its `web_root`, which in turn uses the path to build up it's api URLs. * Use dict.get for Sabnzbd web_root path config --- homeassistant/components/sabnzbd/__init__.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index f5870e42490..bf5e90e21f1 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -10,6 +10,7 @@ from homeassistant.const import ( CONF_HOST, CONF_API_KEY, CONF_NAME, + CONF_PATH, CONF_PORT, CONF_SENSORS, CONF_SSL, @@ -69,6 +70,7 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PATH): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_SENSORS): vol.All( @@ -102,17 +104,20 @@ async def async_configure_sabnzbd( host = config[CONF_HOST] port = config[CONF_PORT] + web_root = config.get(CONF_PATH) uri_scheme = "https" if use_ssl else "http" base_url = BASE_URL_FORMAT.format(uri_scheme, host, port) if api_key is None: conf = await hass.async_add_job(load_json, hass.config.path(CONFIG_FILE)) api_key = conf.get(base_url, {}).get(CONF_API_KEY, "") - sab_api = SabnzbdApi(base_url, api_key, session=async_get_clientsession(hass)) + sab_api = SabnzbdApi( + base_url, api_key, web_root=web_root, session=async_get_clientsession(hass) + ) if await async_check_sabnzbd(sab_api): async_setup_sabnzbd(hass, sab_api, config, name) else: - async_request_configuration(hass, config, base_url) + async_request_configuration(hass, config, base_url, web_root) async def async_setup(hass, config): @@ -181,7 +186,7 @@ def async_setup_sabnzbd(hass, sab_api, config, name): @callback -def async_request_configuration(hass, config, host): +def async_request_configuration(hass, config, host, web_root): """Request configuration steps from the user.""" from pysabnzbd import SabnzbdApi @@ -197,7 +202,9 @@ def async_request_configuration(hass, config, host): async def async_configuration_callback(data): """Handle configuration changes.""" api_key = data.get(CONF_API_KEY) - sab_api = SabnzbdApi(host, api_key, session=async_get_clientsession(hass)) + sab_api = SabnzbdApi( + host, api_key, web_root=web_root, session=async_get_clientsession(hass) + ) if not await async_check_sabnzbd(sab_api): return From 178d0d2099d1683b95dd8c3d12c86e3bc2a56160 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 20 Aug 2019 17:49:21 +0200 Subject: [PATCH 208/273] Update devcontainer.json --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 595506c8ccc..ff2d586fc5a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,7 @@ "postCreateCommand": "pip3 install -e .", "appPort": 8123, "runArgs": [ - "-e", "GIT_EDITOR='code --wait'" + "-e", "GIT_EDITOR=\"code --wait\"" ], "extensions": [ "ms-python.python", From a347a41d3c4fb54011778e214145288cc8e2ba17 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Aug 2019 09:19:00 -0700 Subject: [PATCH 209/273] Add strings for traccar config flow (#26089) * Add strings for traccar config flow * Update strings.json --- homeassistant/components/traccar/strings.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 homeassistant/components/traccar/strings.json diff --git a/homeassistant/components/traccar/strings.json b/homeassistant/components/traccar/strings.json new file mode 100644 index 00000000000..19f4eb0da22 --- /dev/null +++ b/homeassistant/components/traccar/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "Traccar", + "step": { + "user": { + "title": "Set up Traccar", + "description": "Are you sure you want to set up Traccar?" + } + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary.", + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Traccar." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to setup the webhook feature in Traccar.\n\nUse the following url: `{webhook_url}`\n\nSee [the documentation]({docs_url}) for further details." + } + } +} From 0e4504296e8aa811bf9a2ea4002cb1478d17c977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Vran=C3=ADk?= Date: Tue, 20 Aug 2019 18:26:15 +0200 Subject: [PATCH 210/273] Update pyrainbird to version 0.2.1 to fix zone number (#26064) * Update pyrainbird to version 0.2.0 to fix zone number issue: - home-assistant/home-assistant/issues/24519 - jbarrancos/pyrainbird/issues/5 - https://community.home-assistant.io/t/rainbird-zone-switches-5-8-dont-correspond/104705 * requirements_all.txt regenerated * code formatting * code formatting * response checking * fixed switch state * pyrainbird version bump * formatting * version bump * if instead elif --- homeassistant/components/rainbird/__init__.py | 5 ++--- homeassistant/components/rainbird/manifest.json | 2 +- homeassistant/components/rainbird/sensor.py | 6 +++++- homeassistant/components/rainbird/switch.py | 14 +++++++++++--- requirements_all.txt | 2 +- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index 3ae1c8bf585..1d8ed8e37b1 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -29,13 +29,12 @@ def setup(hass, config): from pyrainbird import RainbirdController - controller = RainbirdController() - controller.setConfig(server, password) + controller = RainbirdController(server, password) _LOGGER.debug("Rain Bird Controller set to: %s", server) initial_status = controller.currentIrrigation() - if initial_status == -1: + if initial_status and initial_status["type"] != "CurrentStationsActiveResponse": _LOGGER.error("Error getting state. Possible configuration issues") return False diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index 24113d62534..584ea22afe2 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -3,7 +3,7 @@ "name": "Rainbird", "documentation": "https://www.home-assistant.io/components/rainbird", "requirements": [ - "pyrainbird==0.1.6" + "pyrainbird==0.2.1" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/rainbird/sensor.py b/homeassistant/components/rainbird/sensor.py index d59ea3b0fec..2d4549a21d5 100644 --- a/homeassistant/components/rainbird/sensor.py +++ b/homeassistant/components/rainbird/sensor.py @@ -56,7 +56,11 @@ class RainBirdSensor(Entity): """Get the latest data and updates the states.""" _LOGGER.debug("Updating sensor: %s", self._name) if self._sensor_type == "rainsensor": - self._state = self._controller.currentRainSensorState() + result = self._controller.currentRainSensorState() + if result and result["type"] == "CurrentRainSensorStateResponse": + self._state = result["sensorState"] + else: + self._state = None @property def name(self): diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index 94b37c52fb7..a1b82bc1af7 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -70,15 +70,23 @@ class RainBirdSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn the switch on.""" - self._rainbird.startIrrigation(int(self._zone), int(self._duration)) + response = self._rainbird.startIrrigation(int(self._zone), int(self._duration)) + if response and response["type"] == "AcknowledgeResponse": + self._state = True def turn_off(self, **kwargs): """Turn the switch off.""" - self._rainbird.stopIrrigation() + response = self._rainbird.stopIrrigation() + if response and response["type"] == "AcknowledgeResponse": + self._state = False def get_device_status(self): """Get the status of the switch from Rain Bird Controller.""" - return self._rainbird.currentIrrigation() == self._zone + response = self._rainbird.currentIrrigation() + if response is None: + return None + if isinstance(response, dict) and "sprinklers" in response: + return response["sprinklers"][self._zone] def update(self): """Update switch status.""" diff --git a/requirements_all.txt b/requirements_all.txt index 6cd7f5d2822..0b7346031d4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1359,7 +1359,7 @@ pyqwikswitch==0.93 pyrail==0.0.3 # homeassistant.components.rainbird -pyrainbird==0.1.6 +pyrainbird==0.2.1 # homeassistant.components.recswitch pyrecswitch==1.0.2 From daa0330da4b4864df8c845bb160a6ad1bcad1936 Mon Sep 17 00:00:00 2001 From: legacycode Date: Tue, 20 Aug 2019 19:35:39 +0200 Subject: [PATCH 211/273] Update pylacrosse library to version 0.4.0 (#26088) * Updated pylacrosse library to version 0.4.0. Adding support for remote serial port. * Generated new requirements_all.txt file. --- homeassistant/components/lacrosse/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lacrosse/manifest.json b/homeassistant/components/lacrosse/manifest.json index 4716b3cb548..99dd4889213 100644 --- a/homeassistant/components/lacrosse/manifest.json +++ b/homeassistant/components/lacrosse/manifest.json @@ -3,7 +3,7 @@ "name": "Lacrosse", "documentation": "https://www.home-assistant.io/components/lacrosse", "requirements": [ - "pylacrosse==0.3.1" + "pylacrosse==0.4.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 0b7346031d4..746c23bc28a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1234,7 +1234,7 @@ pykira==0.1.1 pykwb==0.0.8 # homeassistant.components.lacrosse -pylacrosse==0.3.1 +pylacrosse==0.4.0 # homeassistant.components.lastfm pylast==3.1.0 From 97d3f49bb83f385db78c9e46f23cd32469c3a303 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 20 Aug 2019 19:37:55 +0200 Subject: [PATCH 212/273] Docker ADR (#26085) * Migrate Docker image to Hass.io / Multiarch * Fix sudo * Update CODEOWNERS * Fix manifest * Add more logic * fix handling * Move dockerfile * Modify options --- .devcontainer/devcontainer.json | 7 +- CODEOWNERS | 4 - Dockerfile | 38 ------ .devcontainer/Dockerfile => Dockerfile.dev | 1 + azure-pipelines-release.yml | 58 ++++++++- script/hassfest/codeowners.py | 4 - virtualization/Docker/Dockerfile.dev | 64 --------- virtualization/Docker/scripts/libcec | 47 ------- virtualization/Docker/scripts/locales | 12 -- virtualization/Docker/scripts/openalpr | 32 ----- virtualization/Docker/scripts/ssocr | 24 ---- virtualization/Docker/scripts/tellstick | 17 --- virtualization/Docker/setup_docker_prereqs | 84 ------------ virtualization/vagrant/Vagrantfile | 24 ---- virtualization/vagrant/config/.placeholder | 0 .../vagrant/home-assistant@.service | 23 ---- virtualization/vagrant/provision.bat | 50 -------- virtualization/vagrant/provision.sh | 121 ------------------ 18 files changed, 62 insertions(+), 548 deletions(-) delete mode 100644 Dockerfile rename .devcontainer/Dockerfile => Dockerfile.dev (97%) delete mode 100644 virtualization/Docker/Dockerfile.dev delete mode 100755 virtualization/Docker/scripts/libcec delete mode 100755 virtualization/Docker/scripts/locales delete mode 100755 virtualization/Docker/scripts/openalpr delete mode 100755 virtualization/Docker/scripts/ssocr delete mode 100755 virtualization/Docker/scripts/tellstick delete mode 100755 virtualization/Docker/setup_docker_prereqs delete mode 100644 virtualization/vagrant/Vagrantfile delete mode 100644 virtualization/vagrant/config/.placeholder delete mode 100644 virtualization/vagrant/home-assistant@.service delete mode 100644 virtualization/vagrant/provision.bat delete mode 100755 virtualization/vagrant/provision.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ff2d586fc5a..22bd4384b23 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,11 +2,12 @@ { "name": "Home Assistant Dev", "context": "..", - "dockerFile": "Dockerfile", + "dockerFile": "../Dockerfile.dev", "postCreateCommand": "pip3 install -e .", "appPort": 8123, "runArgs": [ - "-e", "GIT_EDITOR=\"code --wait\"" + "-e", + "GIT_EDITOR=\"code --wait\"" ], "extensions": [ "ms-python.python", @@ -31,4 +32,4 @@ "!include_dir_merge_named scalar" ] } -} +} \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS index 1425c476478..3d17b4f9136 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -9,10 +9,6 @@ homeassistant/*.py @home-assistant/core homeassistant/helpers/* @home-assistant/core homeassistant/util/* @home-assistant/core -# Virtualization -Dockerfile @home-assistant/docker -virtualization/Docker/* @home-assistant/docker - # Other code homeassistant/scripts/check_config.py @kellerza diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index ebd802374eb..00000000000 --- a/Dockerfile +++ /dev/null @@ -1,38 +0,0 @@ -# Notice: -# When updating this file, please also update virtualization/Docker/Dockerfile.dev -# This way, the development image and the production image are kept in sync. - -FROM python:3.7-buster -LABEL maintainer="Paulus Schoutsen " - -# Uncomment any of the following lines to disable the installation. -#ENV INSTALL_TELLSTICK no -#ENV INSTALL_OPENALPR no -#ENV INSTALL_FFMPEG no -#ENV INSTALL_LIBCEC no -#ENV INSTALL_SSOCR no -#ENV INSTALL_DLIB no -#ENV INSTALL_IPERF3 no -#ENV INSTALL_LOCALES no - -VOLUME /config - -WORKDIR /usr/src/app - -# Copy build scripts -COPY virtualization/Docker/ virtualization/Docker/ -RUN virtualization/Docker/setup_docker_prereqs - -# Install hass component dependencies -COPY requirements_all.txt requirements_all.txt -RUN pip3 install --no-cache-dir -r requirements_all.txt && \ - pip3 install --no-cache-dir mysqlclient psycopg2 cchardet cython tensorflow - -# Copy source -COPY . . - -EXPOSE 8123 -EXPOSE 8300 -EXPOSE 51827 - -CMD [ "python", "-m", "homeassistant", "--config", "/config" ] diff --git a/.devcontainer/Dockerfile b/Dockerfile.dev similarity index 97% rename from .devcontainer/Dockerfile rename to Dockerfile.dev index 3bfc7e94148..00f5576bdbb 100644 --- a/.devcontainer/Dockerfile +++ b/Dockerfile.dev @@ -16,6 +16,7 @@ RUN apt-get update \ WORKDIR /usr/src +# Setup hass-release RUN git clone --depth 1 https://github.com/home-assistant/hass-release \ && cd hass-release \ && pip3 install -e . diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 13a031fda15..1b547d5c609 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -60,7 +60,7 @@ stages: - script: | export TWINE_USERNAME="$(twineUser)" export TWINE_PASSWORD="$(twinePassword)" - + twine upload dist/* --skip-existing displayName: 'Upload pypi' - job: 'ReleaseDocker' @@ -150,3 +150,59 @@ stages: git commit -am "Bump Home Assistant $version" git push displayName: 'Update version files' + - job: 'ReleaseDocker' + pool: + vmImage: 'ubuntu-latest' + steps: + - script: | + set -e + export DOCKER_CLI_EXPERIMENTAL=enabled + + function create_manifest() { + local tag_l=$1 + local tag_r=$2 + + sudo docker manifest create homeassistant/home-assistant:${tag_l} \ + homeassistant/amd64-homeassistant:${tag_r} \ + homeassistant/i386-homeassistant:${tag_r} \ + homeassistant/armhf-homeassistant:${tag_r} \ + homeassistant/armv7-homeassistant:${tag_r} \ + homeassistant/aarch64-homeassistant:${tag_r} + + sudo docker manifest annotate homeassistant/home-assistant:${tag_l} \ + homeassistant/amd64-homeassistant:${tag_r} \ + --os linux --arch amd64 + + sudo docker manifest annotate homeassistant/home-assistant:${tag_l} \ + homeassistant/i386-homeassistant:${tag_r} \ + --os linux --arch i386 + + sudo docker manifest annotate homeassistant/home-assistant:${tag_l} \ + homeassistant/armhf-homeassistant:${tag_r} \ + --os linux --arch arm --variant=v6 + + sudo docker manifest annotate homeassistant/home-assistant:${tag_l} \ + homeassistant/armv7-homeassistant:${tag_r} \ + --os linux --arch arm --variant=v7 + + sudo docker manifest annotate homeassistant/home-assistant:${tag_l} \ + homeassistant/aarch64-homeassistant:${tag_r} \ + --os linux --arch arm64 --variant=v8 + + sudo docker manifest push --purge homeassistant/home-assistant:${tag_l} + } + + # Create version tag + create_manifest "$(Build.SourceBranchName)" "$(Build.SourceBranchName)" + + # Create general tags + if [[ "$version" =~ d ]]; then + create_manifest "dev" "$(Build.SourceBranchName)" + elif [[ "$version" =~ b ]]; then + create_manifest "beta" "$(Build.SourceBranchName)" + else + create_manifest "stable" "$(Build.SourceBranchName)" + create_manifest "latest" "$(Build.SourceBranchName)" + fi + + displayName: 'Create Meta-Image' diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py index 07a1d827b33..1341bd75d1b 100755 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -15,10 +15,6 @@ homeassistant/*.py @home-assistant/core homeassistant/helpers/* @home-assistant/core homeassistant/util/* @home-assistant/core -# Virtualization -Dockerfile @home-assistant/docker -virtualization/Docker/* @home-assistant/docker - # Other code homeassistant/scripts/check_config.py @kellerza diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev deleted file mode 100644 index 2e98c53cdf1..00000000000 --- a/virtualization/Docker/Dockerfile.dev +++ /dev/null @@ -1,64 +0,0 @@ -# Dockerfile for development -# Based on the production Dockerfile, but with development additions. -# Keep this file as close as possible to the production Dockerfile, so the environments match. - -FROM python:3.7-buster -LABEL maintainer="Paulus Schoutsen " - -# Uncomment any of the following lines to disable the installation. -#ENV INSTALL_TELLSTICK no -#ENV INSTALL_OPENALPR no -#ENV INSTALL_FFMPEG no -#ENV INSTALL_LIBCEC no -#ENV INSTALL_COAP no -#ENV INSTALL_SSOCR no -#ENV INSTALL_DLIB no -#ENV INSTALL_IPERF3 no -#ENV INSTALL_LOCALES no - -VOLUME /config - -WORKDIR /usr/src/app - -# Copy build scripts -COPY virtualization/Docker/ virtualization/Docker/ -RUN virtualization/Docker/setup_docker_prereqs - -# Install hass component dependencies -COPY requirements_all.txt requirements_all.txt - -RUN pip3 install --no-cache-dir -r requirements_all.txt && \ - pip3 install --no-cache-dir mysqlclient psycopg2 cchardet cython - -# BEGIN: Development additions - -# Install git -RUN apt-get update \ - && apt-get install -y --no-install-recommends git \ - && rm -rf /var/lib/apt/lists/* - -# Install nodejs -RUN curl -sL https://deb.nodesource.com/setup_7.x | bash - && \ - apt-get install -y nodejs - -# Install tox -RUN pip3 install --no-cache-dir tox - -# Copy over everything required to run tox -COPY requirements_test_all.txt setup.cfg setup.py tox.ini ./ -COPY homeassistant/const.py homeassistant/const.py - -# Prefetch dependencies for tox -COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt -RUN tox -e py37 --notest - -# END: Development additions - -# Copy source -COPY . . - -EXPOSE 8123 -EXPOSE 8300 -EXPOSE 51827 - -CMD [ "python", "-m", "homeassistant", "--config", "/config" ] diff --git a/virtualization/Docker/scripts/libcec b/virtualization/Docker/scripts/libcec deleted file mode 100755 index 481b3e700ac..00000000000 --- a/virtualization/Docker/scripts/libcec +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/sh -# Sets up libcec. -# Dependencies that need to be installed: -# apt-get install cmake libudev-dev libxrandr-dev swig - -# Stop on errors -set -e - -# Load required information about the current python environment -PYTHON_LIBDIR=$(python -c 'from distutils import sysconfig; print(sysconfig.get_config_var("LIBDIR"))') -PYTHON_LDLIBRARY=$(python -c 'from distutils import sysconfig; print(sysconfig.get_config_var("LDLIBRARY"))') -PYTHON_LIBRARY="${PYTHON_LIBDIR}/${PYTHON_LDLIBRARY}" -PYTHON_INCLUDE_DIR=$(python -c 'from distutils import sysconfig; print(sysconfig.get_python_inc())') - -cd /usr/src/app/ -mkdir -p build && cd build - -if [ ! -d libcec ]; then - git clone --branch release --depth 1 https://github.com/Pulse-Eight/libcec.git -fi - -cd libcec -git checkout release -git pull -git submodule update --init src/platform - -# Build libcec platform libs -( - mkdir -p src/platform/build - cd src/platform/build - cmake .. - make - make install -) - -# Build libcec -( - mkdir -p build && cd build - - cmake \ - -DPYTHON_LIBRARY="${PYTHON_LIBRARY}" \ - -DPYTHON_INCLUDE_DIR="${PYTHON_INCLUDE_DIR}" \ - .. - make -j$(nproc) - make install - ldconfig -) diff --git a/virtualization/Docker/scripts/locales b/virtualization/Docker/scripts/locales deleted file mode 100755 index cbbe0341575..00000000000 --- a/virtualization/Docker/scripts/locales +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -# Sets up locales. - -# Stop on errors -set -e - -apt-get update -apt-get install -y --no-install-recommends locales - -# Set the locale -sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen -locale-gen diff --git a/virtualization/Docker/scripts/openalpr b/virtualization/Docker/scripts/openalpr deleted file mode 100755 index 38669f8175b..00000000000 --- a/virtualization/Docker/scripts/openalpr +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -# Sets up openalpr. - -# Stop on errors -set -e - -PACKAGES=( - # homeassistant.components.image_processing.openalpr_local - libopencv-dev libtesseract-dev libleptonica-dev liblog4cplus-dev -) - -apt-get install -y --no-install-recommends ${PACKAGES[@]} - -cd /usr/src/app/ -mkdir -p build && cd build - -# Clone the latest code from GitHub -git clone --depth 1 https://github.com/openalpr/openalpr.git openalpr - -# Setup the build directory -cd openalpr/src/ -mkdir -p build -cd build - -# Setup the compile environment -cmake -DWITH_TESTS=FALSE -DWITH_BINDING_JAVA=FALSE --DWITH_BINDING_PYTHON=FALSE --DWITH_BINDING_GO=FALSE -DWITH_DAEMON=FALSE -DCMAKE_INSTALL_PREFIX:PATH=/usr/local .. - -# compile the library -make -j$(nproc) - -# Install the binaries/libraries to your local system (prefix is /usr/local) -make install diff --git a/virtualization/Docker/scripts/ssocr b/virtualization/Docker/scripts/ssocr deleted file mode 100755 index 6778bcab90d..00000000000 --- a/virtualization/Docker/scripts/ssocr +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -# Sets up ssocr to support Seven Segments Display. - -# Stop on errors -set -e - -PACKAGES=( - libimlib2 libimlib2-dev -) - -apt-get install -y --no-install-recommends ${PACKAGES[@]} - -cd /usr/src/app/ -mkdir -p build && cd build - -# Clone the latest code from GitHub -git clone --depth 1 https://github.com/auerswal/ssocr.git ssocr -cd ssocr/ - -# Compile the library -make -j$(nproc) - -# Install the binaries/libraries to your local system (prefix is /usr/local) -make install diff --git a/virtualization/Docker/scripts/tellstick b/virtualization/Docker/scripts/tellstick deleted file mode 100755 index d35e1cac2db..00000000000 --- a/virtualization/Docker/scripts/tellstick +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -# Sets up tellstick. - -# Stop on errors -set -e - -PACKAGES=( - # homeassistant.components.tellstick - libtelldus-core2 socat -) - -# Add Tellstick repository -echo "deb http://download.telldus.com/debian/ stable main" >> /etc/apt/sources.list.d/telldus.list -wget -qO - http://download.telldus.com/debian/telldus-public.key | apt-key add - - -apt-get update -apt-get install -y --no-install-recommends ${PACKAGES[@]} diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs deleted file mode 100755 index 62ac73d366e..00000000000 --- a/virtualization/Docker/setup_docker_prereqs +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash -# Install requirements and build dependencies for Home Assistant in Docker. - -# Stop on errors -set -e - -INSTALL_TELLSTICK="${INSTALL_TELLSTICK:-yes}" -INSTALL_OPENALPR="${INSTALL_OPENALPR:-yes}" -INSTALL_LIBCEC="${INSTALL_LIBCEC:-yes}" -INSTALL_SSOCR="${INSTALL_SSOCR:-yes}" -INSTALL_DLIB="${INSTALL_DLIB:-yes}" -INSTALL_LOCALES="${INSTALL_LOCALES:-yes}" - -# Required debian packages for running hass or components -PACKAGES=( - # build-essential is required for python pillow module on non-x86_64 arch - build-essential - # homeassistant.components.image_processing.openalpr_local - libxrandr-dev - # homeassistant.components.device_tracker.nmap_tracker - nmap net-tools libcurl3-dev - # homeassistant.components.device_tracker.bluetooth_tracker - bluetooth libglib2.0-dev libbluetooth-dev - # homeassistant.components.device_tracker.owntracks - libsodium23 - # homeassistant.components.zwave - libudev-dev - # homeassistant.components.homekit_controller - libmpc-dev libmpfr-dev libgmp-dev - # homeassistant.components.ffmpeg - ffmpeg - # homeassistant.components.stream - libavformat-dev libavcodec-dev libavdevice-dev - libavutil-dev libswscale-dev libswresample-dev libavfilter-dev - # homeassistant.components.sensor.iperf3 - iperf3 -) - -# Required debian packages for building dependencies -PACKAGES_DEV=( - cmake - git - swig -) - -# Install packages -apt-get update -apt-get install -y --no-install-recommends ${PACKAGES[@]} ${PACKAGES_DEV[@]} - -# This is a list of scripts that install additional dependencies. If you only -# need to install a package from the official debian repository, just add it -# to the list above. Only create a script if you need compiling, manually -# downloading or a 3rd party repository. -if [ "$INSTALL_TELLSTICK" == "yes" ]; then - virtualization/Docker/scripts/tellstick -fi - -if [ "$INSTALL_OPENALPR" == "yes" ]; then - virtualization/Docker/scripts/openalpr -fi - -if [ "$INSTALL_LIBCEC" == "yes" ]; then - virtualization/Docker/scripts/libcec -fi - -if [ "$INSTALL_SSOCR" == "yes" ]; then - virtualization/Docker/scripts/ssocr -fi - -if [ "$INSTALL_DLIB" == "yes" ]; then - pip3 install --no-cache-dir "dlib>=19.5" -fi - -if [ "$INSTALL_LOCALES" == "yes" ]; then - virtualization/Docker/scripts/locales -fi - -# Remove packages -apt-get remove -y --purge ${PACKAGES_DEV[@]} -apt-get -y --purge autoremove - -# Cleanup -apt-get clean -rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/src/app/build/ diff --git a/virtualization/vagrant/Vagrantfile b/virtualization/vagrant/Vagrantfile deleted file mode 100644 index d3974d51a7a..00000000000 --- a/virtualization/vagrant/Vagrantfile +++ /dev/null @@ -1,24 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -Vagrant.configure(2) do |config| - config.vm.box = "debian/contrib-stretch64" - config.vm.synced_folder "../../", "/home-assistant" - config.vm.synced_folder "./config", "/root/.homeassistant" - config.vm.network "forwarded_port", guest: 8123, host: 8123 - config.vm.provision "fix-no-tty", type: "shell" do |shell| - shell.path = "provision.sh" - end - config.vm.provider :virtualbox do |vb| - vb.cpus = 2 - vb.customize ['modifyvm', :id, '--memory', '1024'] - end - config.vm.provider :hyperv do |h, override| - override.vm.box = "generic/debian9" - override.vm.hostname = "contrib-stretch" - h.vmname = "home-assistant" - h.cpus = 2 - h.memory = 1024 - h.maxmemory = 1024 - end -end diff --git a/virtualization/vagrant/config/.placeholder b/virtualization/vagrant/config/.placeholder deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/virtualization/vagrant/home-assistant@.service b/virtualization/vagrant/home-assistant@.service deleted file mode 100644 index 91b7307f30f..00000000000 --- a/virtualization/vagrant/home-assistant@.service +++ /dev/null @@ -1,23 +0,0 @@ -# This is a simple service file for systems with systemd to tun HA as user. -# -# For details please check https://home-assistant.io/getting-started/autostart/ -# -[Unit] -Description=Home Assistant for %i -After=network.target - -[Service] -Type=simple -User=%i -# Enable the following line if you get network-related HA errors during boot -#ExecStartPre=/usr/bin/sleep 60 -# Use `whereis hass` to determine the path of hass -ExecStart=/usr/bin/hass --runner -SendSIGKILL=no -RestartForceExitStatus=100 - -# on vagrant (vboxfs), disable sendfile https://www.virtualbox.org/ticket/9069 -Environment=AIOHTTP_NOSENDFILE=1 - -[Install] -WantedBy=multi-user.target diff --git a/virtualization/vagrant/provision.bat b/virtualization/vagrant/provision.bat deleted file mode 100644 index c8174e939a1..00000000000 --- a/virtualization/vagrant/provision.bat +++ /dev/null @@ -1,50 +0,0 @@ -@echo off -call:main %* -goto:eof - -:usage -echo.############################################################ -echo. -echo.Use `./provision.bat` to interact with HASS. E.g: -echo. -echo.- setup the environment: `./provision.bat start` -echo.- restart HASS process: `./provision.bat restart` -echo.- run test suit: `./provision.bat tests` -echo.- destroy the host and start anew: `./provision.bat recreate` -echo. -echo.Official documentation at https://home-assistant.io/docs/installation/vagrant/ -echo. -echo.############################################################' -goto:eof - -:main -if "%*"=="setup" ( - if exist setup_done del setup_done - vagrant up --provision - copy /y nul setup_done -) else ( -if "%*"=="tests" ( - copy /y nul run_tests - vagrant provision -) else ( -if "%*"=="restart" ( - copy /y nul restart - vagrant provision -) else ( -if "%*"=="start" ( - vagrant up --provision -) else ( -if "%*"=="stop" ( - vagrant halt -) else ( -if "%*"=="destroy" ( - vagrant destroy -f -) else ( -if "%*"=="recreate" ( - if exist setup_done del setup_done - if exist restart del restart - vagrant destroy -f - vagrant up --provision -) else ( - call:usage -))))))) diff --git a/virtualization/vagrant/provision.sh b/virtualization/vagrant/provision.sh deleted file mode 100755 index 1d2eecddc73..00000000000 --- a/virtualization/vagrant/provision.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/bin/bash -set -e - -readonly SETUP_DONE='/home-assistant/virtualization/vagrant/setup_done' -readonly RUN_TESTS='/home-assistant/virtualization/vagrant/run_tests' -readonly RESTART='/home-assistant/virtualization/vagrant/restart' - -usage() { - echo '############################################################ - -Use `./provision.sh` to interact with HASS. E.g: - -- setup the environment: `./provision.sh start` -- restart HASS process: `./provision.sh restart` -- run test suit: `./provision.sh tests` -- destroy the host and start anew: `./provision.sh recreate` - -Official documentation at https://home-assistant.io/docs/installation/vagrant/ - -############################################################' -} - -print_done() { - echo '############################################################ - - -HASS running => http://localhost:8123/ - -' -} - -setup_error() { - echo '############################################################ -Something is off... maybe setup did not complete properly? -Please ensure setup did run correctly at least once. - -To run setup again: `./provision.sh setup` - -############################################################' - exit 1 -} - -setup() { - local hass_path='/root/venv/bin/hass' - local systemd_bin_path='/usr/bin/hass' - # Setup systemd - cp /home-assistant/virtualization/vagrant/home-assistant@.service \ - /etc/systemd/system/home-assistant.service - systemctl --system daemon-reload - systemctl enable home-assistant - systemctl stop home-assistant - # Install packages - apt-get update - apt-get install -y git rsync python3-dev python3-pip libssl-dev libffi-dev - pip3 install --upgrade virtualenv - virtualenv ~/venv - source ~/venv/bin/activate - pip3 install --upgrade tox - /home-assistant/script/setup - if ! [ -f $systemd_bin_path ]; then - ln -s $hass_path $systemd_bin_path - fi - touch $SETUP_DONE - print_done - usage -} - -run_tests() { - rm -f $RUN_TESTS - echo '############################################################' - echo; echo "Running test suite, hang on..."; echo; echo - if ! systemctl stop home-assistant; then - setup_error - fi - source ~/venv/bin/activate - rsync -a --delete \ - --exclude='*.tox' \ - --exclude='*.git' \ - --exclude='.vagrant' \ - --exclude='lib64' \ - --exclude='bin/python' \ - --exclude='bin/python3' \ - /home-assistant/ /home-assistant-tests/ - cd /home-assistant-tests && tox || true - echo '############################################################' -} - -restart() { - echo "Restarting Home Assistant..." - if ! systemctl restart home-assistant; then - setup_error - else - echo "done" - fi - rm $RESTART -} - -main() { - # If a parameter is provided, we assume it's the user interacting - # with the provider script... - case $1 in - "setup") rm -f setup_done; vagrant up --provision && touch setup_done; exit ;; - "tests") touch run_tests; vagrant provision ; exit ;; - "restart") touch restart; vagrant provision ; exit ;; - "start") vagrant up --provision ; exit ;; - "stop") vagrant halt ; exit ;; - "destroy") vagrant destroy -f ; exit ;; - "recreate") rm -f setup_done restart; vagrant destroy -f; \ - vagrant up --provision; exit ;; - esac - # ...otherwise we assume it's the Vagrant provisioner - if [ $(hostname) != "contrib-jessie" ] && [ $(hostname) != "contrib-stretch" ]; then usage; exit; fi - if ! [ -f $SETUP_DONE ]; then setup; fi - if [ -f $RESTART ]; then restart; fi - if [ -f $RUN_TESTS ]; then run_tests; fi - if ! systemctl start home-assistant; then - setup_error - fi -} - -main $* From 33c35a6c3c3675a16dae509c3df821ba1b1c3930 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 20 Aug 2019 19:43:39 +0200 Subject: [PATCH 213/273] Bump geniushub client (#26084) * bump geniushub client * delint * remove unsused lint hints --- .../components/geniushub/__init__.py | 54 +++++++++--- .../components/geniushub/binary_sensor.py | 44 +++------- homeassistant/components/geniushub/climate.py | 29 ++---- .../components/geniushub/manifest.json | 2 +- homeassistant/components/geniushub/sensor.py | 88 +++++-------------- .../components/geniushub/water_heater.py | 62 ++++++------- requirements_all.txt | 2 +- 7 files changed, 113 insertions(+), 168 deletions(-) diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index e4b723d595b..45f3f91cd6d 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -1,17 +1,23 @@ """Support for a Genius Hub system.""" from datetime import timedelta import logging +from typing import Awaitable import aiohttp import voluptuous as vol -from geniushubclient import GeniusHubClient +from geniushubclient import GeniusHub from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.core import callback 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.dispatcher import ( + async_dispatcher_send, + async_dispatcher_connect, +) +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) @@ -45,7 +51,7 @@ async def async_setup(hass, hass_config): broker = GeniusBroker(hass, args, kwargs) try: - await broker._client.hub.update() # pylint: disable=protected-access + await broker.client.update() except aiohttp.ClientResponseError as err: _LOGGER.error("Setup failed, check your configuration, %s", err) return False @@ -58,7 +64,7 @@ async def async_setup(hass, hass_config): async_load_platform(hass, platform, DOMAIN, {}, hass_config) ) - if broker._client.api_version == 3: # pylint: disable=protected-access + if broker.client.api_version == 3: # pylint: disable=no-member for platform in ["sensor", "binary_sensor"]: hass.async_create_task( async_load_platform(hass, platform, DOMAIN, {}, hass_config) @@ -72,27 +78,53 @@ class GeniusBroker: def __init__(self, hass, args, kwargs): """Initialize the geniushub client.""" - self._hass = hass - self._client = hass.data[DOMAIN]["client"] = GeniusHubClient( + self.hass = hass + self.client = hass.data[DOMAIN]["client"] = GeniusHub( *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() + await self.client.update() except aiohttp.ClientResponseError as err: _LOGGER.warning("Update failed, %s", err) return self.make_debug_log_entries() - async_dispatcher_send(self._hass, DOMAIN) + async_dispatcher_send(self.hass, DOMAIN) def make_debug_log_entries(self): """Make any useful debug log entries.""" # pylint: disable=protected-access _LOGGER.debug( - "Raw JSON: \n\nhub._raw_zones = %s \n\nhub._raw_devices = %s", - self._client.hub._raw_zones, - self._client.hub._raw_devices, + "Raw JSON: \n\nclient._zones = %s \n\nclient._devices = %s", + self.client._zones, + self.client._devices, ) + + +class GeniusEntity(Entity): + """Base for all Genius Hub endtities.""" + + def __init__(self): + """Initialize the entity.""" + self._name = None + + async def async_added_to_hass(self) -> Awaitable[None]: + """Set up a listener when this entity is added to HA.""" + async_dispatcher_connect(self.hass, DOMAIN, self._refresh) + + @callback + def _refresh(self) -> None: + self.async_schedule_update_ha_state(force_refresh=True) + + @property + def name(self) -> str: + """Return the name of the geniushub entity.""" + return self._name + + @property + def should_poll(self) -> bool: + """Return False as geniushub entities should not be polled.""" + return False diff --git a/homeassistant/components/geniushub/binary_sensor.py b/homeassistant/components/geniushub/binary_sensor.py index feb2e0da33e..1cc8cd3f406 100644 --- a/homeassistant/components/geniushub/binary_sensor.py +++ b/homeassistant/components/geniushub/binary_sensor.py @@ -1,10 +1,10 @@ """Support for Genius Hub binary_sensor devices.""" +from typing import Any, Dict + from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.dt import utc_from_timestamp -from . import DOMAIN +from . import DOMAIN, GeniusEntity GH_IS_SWITCH = ["Dual Channel Receiver", "Electric Switch", "Smart Plug"] @@ -14,58 +14,38 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= client = hass.data[DOMAIN]["client"] switches = [ - GeniusBinarySensor(client, d) - for d in client.hub.device_objs - if d.type[:21] in GH_IS_SWITCH + GeniusBinarySensor(d) for d in client.device_objs if d.type[:21] in GH_IS_SWITCH ] async_add_entities(switches) -class GeniusBinarySensor(BinarySensorDevice): +class GeniusBinarySensor(GeniusEntity, BinarySensorDevice): """Representation of a Genius Hub binary_sensor.""" - def __init__(self, client, device): + def __init__(self, device) -> None: """Initialize the binary sensor.""" - self._client = client - self._device = device + super().__init__() + 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): + def is_on(self) -> bool: """Return the status of the sensor.""" return self._device.data["state"]["outputOnOff"] @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" attrs = {} attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] - # noqa; pylint: disable=protected-access - last_comms = self._device._raw_data["childValues"]["lastComms"]["val"] + # pylint: disable=protected-access + last_comms = self._device._raw["childValues"]["lastComms"]["val"] if last_comms != 0: attrs["last_comms"] = utc_from_timestamp(last_comms).isoformat() diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index cee737c09f8..a856e48438f 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -11,10 +11,8 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DOMAIN +from . import DOMAIN, GeniusEntity ATTR_DURATION = "duration" @@ -38,32 +36,24 @@ async def async_setup_platform( client = hass.data[DOMAIN]["client"] entities = [ - GeniusClimateZone(client, z) for z in client.hub.zone_objs if z.type in GH_ZONES + GeniusClimateZone(z) for z in client.zone_objs if z.data["type"] in GH_ZONES ] async_add_entities(entities) -class GeniusClimateZone(ClimateDevice): +class GeniusClimateZone(GeniusEntity, ClimateDevice): """Representation of a Genius Hub climate device.""" - def __init__(self, client, zone): + def __init__(self, zone) -> None: """Initialize the climate device.""" - self._client = client - self._zone = zone + super().__init__() + self._zone = zone if hasattr(self._zone, "occupied"): # has a movement sensor self._preset_modes = list(HA_PRESET_TO_GH) else: self._preset_modes = [PRESET_BOOST] - async def async_added_to_hass(self) -> Awaitable[None]: - """Run when entity about to be added.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) - - @callback - def _refresh(self) -> None: - self.async_schedule_update_ha_state(force_refresh=True) - @property def name(self) -> str: """Return the name of the climate device.""" @@ -75,11 +65,6 @@ class GeniusClimateZone(ClimateDevice): tmp = self._zone.data.items() return {"status": {k: v for k, v in tmp if k in GH_STATE_ATTRS}} - @property - def should_poll(self) -> bool: - """Return False as the geniushub devices should not be polled.""" - return False - @property def icon(self) -> str: """Return the icon to use in the frontend UI.""" @@ -91,7 +76,7 @@ class GeniusClimateZone(ClimateDevice): return self._zone.data["temperature"] @property - def target_temperature(self) -> Optional[float]: + def target_temperature(self) -> float: """Return the temperature we try to reach.""" return self._zone.data["setpoint"] diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index 0721c4ff389..12f7c266840 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.5.8" + "geniushub-client==0.6.5" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 65bfcb7fe9b..5e39be1620a 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -1,13 +1,11 @@ """Support for Genius Hub sensor devices.""" from datetime import timedelta +from typing import Any, Awaitable, Dict 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 homeassistant.util.dt import utc_from_timestamp, utcnow -from . import DOMAIN +from . import DOMAIN, GeniusEntity GH_HAS_BATTERY = ["Room Thermostat", "Genius Valve", "Room Sensor", "Radiator Valve"] @@ -22,44 +20,27 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up the Genius Hub sensor entities.""" client = hass.data[DOMAIN]["client"] - sensors = [ - GeniusBattery(client, d) - for d in client.hub.device_objs - if d.type in GH_HAS_BATTERY - ] + sensors = [GeniusBattery(d) for d in client.device_objs if d.type in GH_HAS_BATTERY] issues = [GeniusIssue(client, i) for i in list(GH_LEVEL_MAPPING)] async_add_entities(sensors + issues, update_before_add=True) -class GeniusBattery(Entity): +class GeniusBattery(GeniusEntity): """Representation of a Genius Hub sensor.""" - def __init__(self, client, device): + def __init__(self, device) -> None: """Initialize the sensor.""" - self._client = client - self._device = device + super().__init__() + 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 icon(self): + def icon(self) -> str: """Return the icon of the sensor.""" - # noqa; pylint: disable=protected-access - values = self._device._raw_data["childValues"] + + values = self._device._raw["childValues"] # pylint: disable=protected-access last_comms = utc_from_timestamp(values["lastComms"]["val"]) if "WakeUp_Interval" in values: @@ -83,78 +64,57 @@ class GeniusBattery(Entity): return icon @property - def device_class(self): + def device_class(self) -> str: """Return the device class of the sensor.""" return DEVICE_CLASS_BATTERY @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """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): + def state(self) -> str: """Return the state of the sensor.""" level = self._device.data["state"].get("batteryLevel", 255) return level if level != 255 else 0 @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" attrs = {} attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] - # noqa; pylint: disable=protected-access - last_comms = self._device._raw_data["childValues"]["lastComms"]["val"] + # pylint: disable=protected-access + last_comms = self._device._raw["childValues"]["lastComms"]["val"] attrs["last_comms"] = utc_from_timestamp(last_comms).isoformat() return {**attrs} -class GeniusIssue(Entity): +class GeniusIssue(GeniusEntity): """Representation of a Genius Hub sensor.""" - def __init__(self, client, level): + def __init__(self, hub, level) -> None: """Initialize the sensor.""" - self._hub = client.hub + super().__init__() + + self._hub = 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): + def state(self) -> str: """Return the number of issues.""" return len(self._issues) @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" return {"{}_list".format(self._level): self._issues} - async def async_update(self): + async def async_update(self) -> Awaitable[None]: """Process the sensor's state data.""" self._issues = [ i["description"] for i in self._hub.issues if i["level"] == self._level diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index feb4235d4dd..1086160e77c 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -1,14 +1,14 @@ """Support for Genius Hub water_heater devices.""" +from typing import Any, Awaitable, Dict, Optional, List + from homeassistant.components.water_heater import ( WaterHeaterDevice, 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 +from . import DOMAIN, GeniusEntity STATE_AUTO = "auto" STATE_MANUAL = "manual" @@ -44,93 +44,81 @@ async def async_setup_platform( client = hass.data[DOMAIN]["client"] entities = [ - GeniusWaterHeater(client, z) - for z in client.hub.zone_objs - if z.type in GH_HEATERS + GeniusWaterHeater(z) for z in client.zone_objs if z.data["type"] in GH_HEATERS ] async_add_entities(entities) -class GeniusWaterHeater(WaterHeaterDevice): +class GeniusWaterHeater(GeniusEntity, WaterHeaterDevice): """Representation of a Genius Hub water_heater device.""" - def __init__(self, client, boiler): + def __init__(self, boiler) -> None: """Initialize the water_heater device.""" - self._client = client - self._boiler = boiler + super().__init__() + self._boiler = boiler 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): + def name(self) -> str: """Return the name of the water_heater device.""" return self._boiler.name @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" - tmp = self._boiler.data.items() - return {"status": {k: v for k, v in tmp if k in GH_STATE_ATTRS}} + return { + "status": { + k: v for k, v in self._boiler.data.items() if k in GH_STATE_ATTRS + } + } @property - def should_poll(self) -> bool: - """Return False as the geniushub devices should not be polled.""" - return False - - @property - def current_temperature(self): + def current_temperature(self) -> Optional[float]: """Return the current temperature.""" return self._boiler.data.get("temperature") @property - def target_temperature(self): + def target_temperature(self) -> float: """Return the temperature we try to reach.""" return self._boiler.data["setpoint"] @property - def min_temp(self): + def min_temp(self) -> float: """Return max valid temperature that can be set.""" return GH_MIN_TEMP @property - def max_temp(self): + def max_temp(self) -> float: """Return max valid temperature that can be set.""" return GH_MAX_TEMP @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" return TEMP_CELSIUS @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" return GH_SUPPORT_FLAGS @property - def operation_list(self): + def operation_list(self) -> List[str]: """Return the list of available operation modes.""" return self._operation_list @property - def current_operation(self): + def current_operation(self) -> str: """Return the current operation mode.""" return GH_STATE_TO_HA[self._boiler.data["mode"]] - async def async_set_operation_mode(self, operation_mode): + async def async_set_operation_mode(self, operation_mode) -> Awaitable[None]: """Set a new operation mode for this boiler.""" await self._boiler.set_mode(HA_OPMODE_TO_GH[operation_mode]) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs) -> Awaitable[None]: """Set a new target temperature for this boiler.""" temperature = kwargs[ATTR_TEMPERATURE] await self._boiler.set_override(temperature, 3600) # 1 hour diff --git a/requirements_all.txt b/requirements_all.txt index 746c23bc28a..657a7912159 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -518,7 +518,7 @@ gearbest_parser==1.0.7 geizhals==0.0.9 # homeassistant.components.geniushub -geniushub-client==0.5.8 +geniushub-client==0.6.5 # homeassistant.components.geo_json_events # homeassistant.components.nsw_rural_fire_service_feed From cf2d927f141e195b1c3ca15ca6c5e625994e208f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Aug 2019 10:46:51 -0700 Subject: [PATCH 214/273] Use init_subclass for Config Entries (#26059) * Use init_subclass for Config Entries * Ignore type --- homeassistant/components/hue/config_flow.py | 3 +- homeassistant/components/met/config_flow.py | 3 +- homeassistant/config_entries.py | 6 ++++ tests/test_config_entries.py | 35 +++++++++++---------- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 1d058d84b61..0b0e3723b13 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -44,8 +44,7 @@ def _find_username_from_config(hass, filename): return None -@config_entries.HANDLERS.register(DOMAIN) -class HueFlowHandler(config_entries.ConfigFlow): +class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Hue config flow.""" VERSION = 1 diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index e903c717e64..795ba57d988 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -17,8 +17,7 @@ def configured_instances(hass): ) -@config_entries.HANDLERS.register(DOMAIN) -class MetFlowHandler(config_entries.ConfigFlow): +class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for Met component.""" VERSION = 1 diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 87bce1a870c..2e1fbea14d1 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -670,6 +670,12 @@ async def _old_conf_migrator(old_config): class ConfigFlow(data_entry_flow.FlowHandler): """Base class for config flows with some helpers.""" + def __init_subclass__(cls, domain=None, **kwargs): + """Initialize a subclass, register if possible.""" + super().__init_subclass__(**kwargs) # type: ignore + if domain is not None: + HANDLERS.register(domain)(cls) + CONNECTION_CLASS = CONN_CLASS_UNKNOWN @staticmethod diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 6c1b00693dd..ca6872a7a2c 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -521,31 +521,32 @@ async def test_discovery_notification(hass): mock_entity_platform(hass, "config_flow.test", None) await async_setup_component(hass, "persistent_notification", {}) - class TestFlow(config_entries.ConfigFlow): - VERSION = 5 + with patch.dict(config_entries.HANDLERS): - async def async_step_discovery(self, user_input=None): - if user_input is not None: - return self.async_create_entry( - title="Test Title", data={"token": "abcd"} - ) - return self.async_show_form(step_id="discovery") + class TestFlow(config_entries.ConfigFlow, domain="test"): + VERSION = 5 + + async def async_step_discovery(self, user_input=None): + if user_input is not None: + return self.async_create_entry( + title="Test Title", data={"token": "abcd"} + ) + return self.async_show_form(step_id="discovery") - with patch.dict(config_entries.HANDLERS, {"test": TestFlow}): result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY} ) - await hass.async_block_till_done() - state = hass.states.get("persistent_notification.config_entry_discovery") - assert state is not None + await hass.async_block_till_done() + state = hass.states.get("persistent_notification.config_entry_discovery") + assert state is not None - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - await hass.async_block_till_done() - state = hass.states.get("persistent_notification.config_entry_discovery") - assert state is None + await hass.async_block_till_done() + state = hass.states.get("persistent_notification.config_entry_discovery") + assert state is None async def test_discovery_notification_not_created(hass): From 7fd53ac912484f5da13c256c84206187a4517805 Mon Sep 17 00:00:00 2001 From: ahertz Date: Tue, 20 Aug 2019 13:53:45 -0400 Subject: [PATCH 215/273] Sonos playlists play media (#26054) * Add handling for Sonos playlists in media_player.play_media * Avoid breaking change by falling back to previous behavior * Use the proper MEDIA_TYPE_PLAYLIST constant. * Addressed comments, restricting media_type to music or playlist --- .../components/sonos/media_player.py | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index a4d461f289f..86e30621334 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -15,6 +15,7 @@ from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_MUSIC, + MEDIA_TYPE_PLAYLIST, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -938,20 +939,35 @@ class SonosEntity(MediaPlayerDevice): """ Send the play_media command to the media player. + If media_type is "playlist", media_id should be a Sonos + Playlist name. Otherwise, media_id should be a URI. + If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. """ - if kwargs.get(ATTR_MEDIA_ENQUEUE): + if media_type == MEDIA_TYPE_MUSIC: + if kwargs.get(ATTR_MEDIA_ENQUEUE): + try: + self.soco.add_uri_to_queue(media_id) + except SoCoUPnPException: + _LOGGER.error( + 'Error parsing media uri "%s", ' + "please check it's a valid media resource " + "supported by Sonos", + media_id, + ) + else: + self.soco.play_uri(media_id) + elif media_type == MEDIA_TYPE_PLAYLIST: try: - self.soco.add_uri_to_queue(media_id) - except SoCoUPnPException: - _LOGGER.error( - 'Error parsing media uri "%s", ' - "please check it's a valid media resource " - "supported by Sonos", - media_id, - ) + playlists = self.soco.get_sonos_playlists() + playlist = next(p for p in playlists if p.title == media_id) + self.soco.clear_queue() + self.soco.add_to_queue(playlist) + self.soco.play_from_queue(0) + except StopIteration: + _LOGGER.error('Could not find a Sonos playlist named "%s"', media_id) else: - self.soco.play_uri(media_id) + _LOGGER.error('Sonos does not support a media type of "%s"', media_type) @soco_error() def join(self, slaves): From d96edea6e297f6789b70bb1677ec3360627320e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 20 Aug 2019 19:54:54 +0200 Subject: [PATCH 216/273] Remove the googlehome integration (#26035) --- .coveragerc | 1 - CODEOWNERS | 1 - .../components/googlehome/__init__.py | 118 ------------------ .../components/googlehome/device_tracker.py | 80 ------------ .../components/googlehome/manifest.json | 12 -- homeassistant/components/googlehome/sensor.py | 91 -------------- requirements_all.txt | 3 - 7 files changed, 306 deletions(-) delete mode 100644 homeassistant/components/googlehome/__init__.py delete mode 100644 homeassistant/components/googlehome/device_tracker.py delete mode 100644 homeassistant/components/googlehome/manifest.json delete mode 100644 homeassistant/components/googlehome/sensor.py diff --git a/.coveragerc b/.coveragerc index e0bbbd66d7c..1d861d69c1d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -241,7 +241,6 @@ omit = homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py homeassistant/components/google_travel_time/sensor.py - homeassistant/components/googlehome/* homeassistant/components/gpmdp/media_player.py homeassistant/components/gpsd/sensor.py homeassistant/components/greeneye_monitor/* diff --git a/CODEOWNERS b/CODEOWNERS index 3d17b4f9136..d08ac85941c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -104,7 +104,6 @@ homeassistant/components/gntp/* @robbiet480 homeassistant/components/google_cloud/* @lufton homeassistant/components/google_translate/* @awarecan homeassistant/components/google_travel_time/* @robbiet480 -homeassistant/components/googlehome/* @ludeeus homeassistant/components/gpsd/* @fabaff homeassistant/components/group/* @home-assistant/core homeassistant/components/gtfs/* @robbiet480 diff --git a/homeassistant/components/googlehome/__init__.py b/homeassistant/components/googlehome/__init__.py deleted file mode 100644 index 01e17708fb3..00000000000 --- a/homeassistant/components/googlehome/__init__.py +++ /dev/null @@ -1,118 +0,0 @@ -"""Support Google Home units.""" -import logging - -import asyncio -import voluptuous as vol -from homeassistant.const import CONF_DEVICES, CONF_HOST -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "googlehome" -CLIENT = "googlehome_client" - -NAME = "GoogleHome" - -CONF_DEVICE_TYPES = "device_types" -CONF_RSSI_THRESHOLD = "rssi_threshold" -CONF_TRACK_ALARMS = "track_alarms" -CONF_TRACK_DEVICES = "track_devices" - -DEVICE_TYPES = [1, 2, 3] -DEFAULT_RSSI_THRESHOLD = -70 - -DEVICE_CONFIG = vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_DEVICE_TYPES, default=DEVICE_TYPES): vol.All( - cv.ensure_list, [vol.In(DEVICE_TYPES)] - ), - vol.Optional(CONF_RSSI_THRESHOLD, default=DEFAULT_RSSI_THRESHOLD): vol.Coerce( - int - ), - vol.Optional(CONF_TRACK_ALARMS, default=False): cv.boolean, - vol.Optional(CONF_TRACK_DEVICES, default=True): cv.boolean, - } -) - - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - {vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_CONFIG])} - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass, config): - """Set up the Google Home component.""" - hass.data[DOMAIN] = {} - hass.data[CLIENT] = GoogleHomeClient(hass) - - for device in config[DOMAIN][CONF_DEVICES]: - hass.data[DOMAIN][device["host"]] = {} - if device[CONF_TRACK_DEVICES]: - hass.async_create_task( - discovery.async_load_platform( - hass, "device_tracker", DOMAIN, device, config - ) - ) - - if device[CONF_TRACK_ALARMS]: - hass.async_create_task( - discovery.async_load_platform(hass, "sensor", DOMAIN, device, config) - ) - - return True - - -class GoogleHomeClient: - """Handle all communication with the Google Home unit.""" - - def __init__(self, hass): - """Initialize the Google Home Client.""" - self.hass = hass - self._connected = None - - async def update_info(self, host): - """Update data from Google Home.""" - from googledevices.api.connect import Cast - - _LOGGER.debug("Updating Google Home info for %s", host) - session = async_get_clientsession(self.hass) - - device_info = await Cast(host, self.hass.loop, session).info() - device_info_data = await device_info.get_device_info() - self._connected = bool(device_info_data) - - self.hass.data[DOMAIN][host]["info"] = device_info_data - - async def update_bluetooth(self, host): - """Update bluetooth from Google Home.""" - from googledevices.api.connect import Cast - - _LOGGER.debug("Updating Google Home bluetooth for %s", host) - session = async_get_clientsession(self.hass) - - bluetooth = await Cast(host, self.hass.loop, session).bluetooth() - await bluetooth.scan_for_devices() - await asyncio.sleep(5) - bluetooth_data = await bluetooth.get_scan_result() - - self.hass.data[DOMAIN][host]["bluetooth"] = bluetooth_data - - async def update_alarms(self, host): - """Update alarms from Google Home.""" - from googledevices.api.connect import Cast - - _LOGGER.debug("Updating Google Home bluetooth for %s", host) - session = async_get_clientsession(self.hass) - - assistant = await Cast(host, self.hass.loop, session).assistant() - alarms_data = await assistant.get_alarms() - - self.hass.data[DOMAIN][host]["alarms"] = alarms_data diff --git a/homeassistant/components/googlehome/device_tracker.py b/homeassistant/components/googlehome/device_tracker.py deleted file mode 100644 index 58350afa430..00000000000 --- a/homeassistant/components/googlehome/device_tracker.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Support for Google Home Bluetooth tacker.""" -from datetime import timedelta -import logging - -from homeassistant.components.device_tracker import DeviceScanner -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.util import slugify - -from . import CLIENT, DOMAIN as GOOGLEHOME_DOMAIN, NAME - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_SCAN_INTERVAL = timedelta(seconds=10) - - -async def async_setup_scanner(hass, config, async_see, discovery_info=None): - """Validate the configuration and return a Google Home scanner.""" - if discovery_info is None: - _LOGGER.warning("To use this you need to configure the 'googlehome' component") - return False - scanner = GoogleHomeDeviceScanner( - hass, hass.data[CLIENT], discovery_info, async_see - ) - return await scanner.async_init() - - -class GoogleHomeDeviceScanner(DeviceScanner): - """This class queries a Google Home unit.""" - - def __init__(self, hass, client, config, async_see): - """Initialize the scanner.""" - self.async_see = async_see - self.hass = hass - self.rssi = config["rssi_threshold"] - self.device_types = config["device_types"] - self.host = config["host"] - self.client = client - - async def async_init(self): - """Further initialize connection to Google Home.""" - await self.client.update_info(self.host) - data = self.hass.data[GOOGLEHOME_DOMAIN][self.host] - info = data.get("info", {}) - connected = bool(info) - if connected: - await self.async_update() - async_track_time_interval( - self.hass, self.async_update, DEFAULT_SCAN_INTERVAL - ) - return connected - - async def async_update(self, now=None): - """Ensure the information from Google Home is up to date.""" - _LOGGER.debug("Checking Devices on %s", self.host) - await self.client.update_bluetooth(self.host) - data = self.hass.data[GOOGLEHOME_DOMAIN][self.host] - info = data.get("info") - bluetooth = data.get("bluetooth") - if info is None or bluetooth is None: - return - google_home_name = info.get("name", NAME) - - for device in bluetooth: - if ( - device["device_type"] not in self.device_types - or device["rssi"] < self.rssi - ): - continue - - name = "{} {}".format(self.host, device["mac_address"]) - - attributes = {} - attributes["btle_mac_address"] = device["mac_address"] - attributes["ghname"] = google_home_name - attributes["rssi"] = device["rssi"] - attributes["source_type"] = "bluetooth" - if device["name"]: - attributes["name"] = device["name"] - - await self.async_see(dev_id=slugify(name), attributes=attributes) diff --git a/homeassistant/components/googlehome/manifest.json b/homeassistant/components/googlehome/manifest.json deleted file mode 100644 index 107e7d634f0..00000000000 --- a/homeassistant/components/googlehome/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "domain": "googlehome", - "name": "Googlehome", - "documentation": "https://www.home-assistant.io/components/googlehome", - "requirements": [ - "googledevices==1.0.2" - ], - "dependencies": [], - "codeowners": [ - "@ludeeus" - ] -} diff --git a/homeassistant/components/googlehome/sensor.py b/homeassistant/components/googlehome/sensor.py deleted file mode 100644 index 6a578e14f5a..00000000000 --- a/homeassistant/components/googlehome/sensor.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Support for Google Home alarm sensor.""" -from datetime import timedelta -import logging - -from homeassistant.const import DEVICE_CLASS_TIMESTAMP -from homeassistant.helpers.entity import Entity -import homeassistant.util.dt as dt_util - -from . import CLIENT, DOMAIN as GOOGLEHOME_DOMAIN, NAME - -SCAN_INTERVAL = timedelta(seconds=10) - -_LOGGER = logging.getLogger(__name__) - -ICON = "mdi:alarm" - -SENSOR_TYPES = {"timer": "Timer", "alarm": "Alarm"} - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the googlehome sensor platform.""" - if discovery_info is None: - _LOGGER.warning("To use this you need to configure the 'googlehome' component") - return - - await hass.data[CLIENT].update_info(discovery_info["host"]) - data = hass.data[GOOGLEHOME_DOMAIN][discovery_info["host"]] - info = data.get("info", {}) - - devices = [] - for condition in SENSOR_TYPES: - device = GoogleHomeAlarm( - hass.data[CLIENT], condition, discovery_info, info.get("name", NAME) - ) - devices.append(device) - - async_add_entities(devices, True) - - -class GoogleHomeAlarm(Entity): - """Representation of a GoogleHomeAlarm.""" - - def __init__(self, client, condition, config, name): - """Initialize the GoogleHomeAlarm sensor.""" - self._host = config["host"] - self._client = client - self._condition = condition - self._name = None - self._state = None - self._available = True - self._name = "{} {}".format(name, SENSOR_TYPES[self._condition]) - - async def async_update(self): - """Update the data.""" - await self._client.update_alarms(self._host) - data = self.hass.data[GOOGLEHOME_DOMAIN][self._host] - - alarms = data.get("alarms")[self._condition] - if not alarms: - self._available = False - return - self._available = True - time_date = dt_util.utc_from_timestamp( - min(element["fire_time"] for element in alarms) / 1000 - ) - self._state = time_date.isoformat() - - @property - def state(self): - """Return the state.""" - return self._state - - @property - def name(self): - """Return the name.""" - return self._name - - @property - def device_class(self): - """Return the device class.""" - return DEVICE_CLASS_TIMESTAMP - - @property - def available(self): - """Return the availability state.""" - return self._available - - @property - def icon(self): - """Return the icon.""" - return ICON diff --git a/requirements_all.txt b/requirements_all.txt index 657a7912159..7b09fdddbe5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -560,9 +560,6 @@ google-cloud-pubsub==0.39.1 # homeassistant.components.google_cloud google-cloud-texttospeech==0.4.0 -# homeassistant.components.googlehome -googledevices==1.0.2 - # homeassistant.components.google_travel_time googlemaps==2.5.1 From 4bce1efeeede25c84337e06d3ef97233e32dea72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 20 Aug 2019 20:55:40 +0300 Subject: [PATCH 217/273] Test with 3.6.0 in Travis (#26039) https://github.com/home-assistant/home-assistant/pull/26030#issuecomment-522298190 --- .travis.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index f54f4027de4..3447571a3e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,14 +16,18 @@ addons: matrix: fast_finish: true include: - - python: "3.6" + - python: "3.6.0" env: TOXENV=lint - - python: "3.6" + dist: trusty + - python: "3.6.0" env: TOXENV=pylint - - python: "3.6" + dist: trusty + - python: "3.6.0" env: TOXENV=typing - - python: "3.6" + dist: trusty + - python: "3.6.0" env: TOXENV=py36 + dist: trusty - python: "3.7" env: TOXENV=py37 From f34b84a5cf0dead8a5057f63bf435a84640f0554 Mon Sep 17 00:00:00 2001 From: Sander Date: Tue, 20 Aug 2019 19:59:01 +0200 Subject: [PATCH 218/273] add id to state attributes (#26086) --- homeassistant/components/homematicip_cloud/device.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 021c264f63f..6ff39c8b3a7 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -11,6 +11,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) ATTR_MODEL_TYPE = "model_type" +ATTR_ID = "id" # RSSI HAP -> Device ATTR_RSSI_DEVICE = "rssi_device" # RSSI Device -> HAP @@ -93,7 +94,8 @@ class HomematicipGenericDevice(Entity): @property def device_state_attributes(self): """Return the state attributes of the generic device.""" - attr = {ATTR_MODEL_TYPE: self._device.modelType} + attr = {ATTR_MODEL_TYPE: self._device.modelType, ATTR_ID: self._device.id} + if hasattr(self._device, "sabotage") and self._device.sabotage: attr[ATTR_SABOTAGE] = self._device.sabotage if hasattr(self._device, "rssiDeviceValue") and self._device.rssiDeviceValue: From 5c91a6cd48cfcb57c64333b6368e6406f8cd8d78 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Aug 2019 12:43:50 -0700 Subject: [PATCH 219/273] Fix open-ui cli arg (#26091) * Fix open-ui cli command * Align add_job typing with async_add_job --- homeassistant/__main__.py | 26 ++++---------------------- homeassistant/core.py | 2 +- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index d21bfb5a71a..8ec2a8c2d3c 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -10,12 +10,7 @@ import threading from typing import List, Dict, Any, TYPE_CHECKING # noqa pylint: disable=unused-import from homeassistant import monkey_patch -from homeassistant.const import ( - __version__, - EVENT_HOMEASSISTANT_START, - REQUIRED_PYTHON_VER, - RESTART_EXIT_CODE, -) +from homeassistant.const import __version__, REQUIRED_PYTHON_VER, RESTART_EXIT_CODE if TYPE_CHECKING: from homeassistant import core @@ -309,23 +304,10 @@ async def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int: log_no_color=args.log_no_color, ) - if args.open_ui: - # Imported here to avoid importing asyncio before monkey patch - from homeassistant.util.async_ import run_callback_threadsafe + if args.open_ui and hass.config.api is not None: + import webbrowser - def open_browser(_: Any) -> None: - """Open the web interface in a browser.""" - if hass.config.api is not None: - import webbrowser - - webbrowser.open(hass.config.api.base_url) - - run_callback_threadsafe( - hass.loop, - hass.bus.async_listen_once, - EVENT_HOMEASSISTANT_START, - open_browser, - ) + hass.add_job(webbrowser.open, hass.config.api.base_url) return await hass.async_run() diff --git a/homeassistant/core.py b/homeassistant/core.py index a205aa401a6..e8e33a0479e 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -276,7 +276,7 @@ class HomeAssistant: self.state = CoreState.running _async_create_timer(self) - def add_job(self, target: Callable[..., None], *args: Any) -> None: + def add_job(self, target: Callable[..., Any], *args: Any) -> None: """Add job to the executor pool. target: target to call. From 000d3d4fdec1e1cf936e75afc43c7ab43e7d808a Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 20 Aug 2019 23:27:59 +0200 Subject: [PATCH 220/273] [bugfix] Exception if vendor's servers are currently unavailable (#26093) --- homeassistant/components/evohome/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index f0e7a26e1f5..05308782362 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -273,10 +273,10 @@ class EvoBroker: else: self.timers["statusUpdated"] = utcnow() - _LOGGER.debug("Status = %s", status) + _LOGGER.debug("Status = %s", status) - # inform the evohome devices that state data has been updated - async_dispatcher_send(self.hass, DOMAIN, {"signal": "refresh"}) + # inform the evohome devices that state data has been updated + async_dispatcher_send(self.hass, DOMAIN, {"signal": "refresh"}) class EvoDevice(Entity): From 21a946009d68238004253ec7b8f9b0e7dac22f48 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 20 Aug 2019 19:06:38 -0400 Subject: [PATCH 221/273] Bump up zha dependencies. (#26097) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 4c45b59aebb..dcf38e63d2f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,9 +4,9 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/zha", "requirements": [ - "bellows-homeassistant==0.9.0", + "bellows-homeassistant==0.9.1", "zha-quirks==0.0.20", - "zigpy-deconz==0.2.1", + "zigpy-deconz==0.2.2", "zigpy-homeassistant==0.7.1", "zigpy-xbee-homeassistant==0.4.0", "zigpy-zigate==0.1.0" diff --git a/requirements_all.txt b/requirements_all.txt index 7b09fdddbe5..78a17c42a45 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -263,7 +263,7 @@ batinfo==0.4.2 beautifulsoup4==4.8.0 # homeassistant.components.zha -bellows-homeassistant==0.9.0 +bellows-homeassistant==0.9.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.5.3 @@ -1980,7 +1980,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.2.1 +zigpy-deconz==0.2.2 # homeassistant.components.zha zigpy-homeassistant==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2829d2018c..6e53f3badd7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -92,7 +92,7 @@ av==6.1.2 axis==25 # homeassistant.components.zha -bellows-homeassistant==0.9.0 +bellows-homeassistant==0.9.1 # homeassistant.components.caldav caldav==0.6.1 From 2fbe01fb33ca291a645a3177cbb3412aa158374f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Aug 2019 16:20:38 -0700 Subject: [PATCH 222/273] Updated frontend to 20190820.1 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index f7c1a4f874f..2337c3cb469 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==20190820.0" + "home-assistant-frontend==20190820.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 74ffb83f884..2c649b007e3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.17 -home-assistant-frontend==20190820.0 +home-assistant-frontend==20190820.1 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 78a17c42a45..70378e90fda 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -624,7 +624,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190820.0 +home-assistant-frontend==20190820.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e53f3badd7..5380f53c979 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -177,7 +177,7 @@ hdate==0.9.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190820.0 +home-assistant-frontend==20190820.1 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 38ce4039c3c6580e13d9ad006f3390e2c9affd7a Mon Sep 17 00:00:00 2001 From: Gerard Date: Wed, 21 Aug 2019 09:11:06 +0200 Subject: [PATCH 223/273] Update bimmer_connected to 0.6.0 (#26098) * Update bimmer_connected to 0.6.0 * Correct file properties --- CODEOWNERS | 1 + .../bmw_connected_drive/__init__.py | 5 +- .../bmw_connected_drive/binary_sensor.py | 65 ++++++++++--------- .../bmw_connected_drive/manifest.json | 7 +- .../components/bmw_connected_drive/sensor.py | 13 ++-- requirements_all.txt | 2 +- 6 files changed, 50 insertions(+), 43 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index d08ac85941c..7a153b3ff82 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -40,6 +40,7 @@ homeassistant/components/azure_event_hub/* @eavanvalkenburg homeassistant/components/bitcoin/* @fabaff homeassistant/components/bizkaibus/* @UgaitzEtxebarria homeassistant/components/blink/* @fronzbot +homeassistant/components/bmw_connected_drive/* @gerard33 homeassistant/components/braviatv/* @robbiet480 homeassistant/components/broadlink/* @danielhiversen homeassistant/components/brunt/* @eavanvalkenburg diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 9b44012e758..c257470bb2d 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -143,7 +143,10 @@ class BMWConnectedDriveAccount: for listener in self._update_listeners: listener() except IOError as exception: - _LOGGER.error("Error updating the vehicle state") + _LOGGER.error( + "Could not connect to the BMW Connected Drive portal. " + "The vehicle state could not be updated." + ) _LOGGER.exception(exception) def add_update_listener(self, listener): diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index d52bec330fb..418ccbabffe 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -9,17 +9,17 @@ from . import DOMAIN as BMW_DOMAIN _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - "lids": ["Doors", "opening"], - "windows": ["Windows", "opening"], - "door_lock_state": ["Door lock state", "safety"], - "lights_parking": ["Parking lights", "light"], - "condition_based_services": ["Condition based services", "problem"], - "check_control_messages": ["Control messages", "problem"], + "lids": ["Doors", "opening", "mdi:car-door"], + "windows": ["Windows", "opening", "mdi:car-door"], + "door_lock_state": ["Door lock state", "safety", "mdi:car-key"], + "lights_parking": ["Parking lights", "light", "mdi:car-parking-lights"], + "condition_based_services": ["Condition based services", "problem", "mdi:wrench"], + "check_control_messages": ["Control messages", "problem", "mdi:car-tire-alert"], } SENSOR_TYPES_ELEC = { - "charging_status": ["Charging status", "power"], - "connection_status": ["Connection status", "plug"], + "charging_status": ["Charging status", "power", "mdi:ev-station"], + "connection_status": ["Connection status", "plug", "mdi:car-electric"], } SENSOR_TYPES_ELEC.update(SENSOR_TYPES) @@ -35,24 +35,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if vehicle.has_hv_battery: _LOGGER.debug("BMW with a high voltage battery") for key, value in sorted(SENSOR_TYPES_ELEC.items()): - device = BMWConnectedDriveSensor( - account, vehicle, key, value[0], value[1] - ) - devices.append(device) + if key in vehicle.available_attributes: + device = BMWConnectedDriveSensor( + account, vehicle, key, value[0], value[1], value[2] + ) + devices.append(device) elif vehicle.has_internal_combustion_engine: _LOGGER.debug("BMW with an internal combustion engine") for key, value in sorted(SENSOR_TYPES.items()): - device = BMWConnectedDriveSensor( - account, vehicle, key, value[0], value[1] - ) - devices.append(device) + if key in vehicle.available_attributes: + device = BMWConnectedDriveSensor( + account, vehicle, key, value[0], value[1], value[2] + ) + devices.append(device) add_entities(devices, True) class BMWConnectedDriveSensor(BinarySensorDevice): """Representation of a BMW vehicle binary sensor.""" - def __init__(self, account, vehicle, attribute: str, sensor_name, device_class): + def __init__( + self, account, vehicle, attribute: str, sensor_name, device_class, icon + ): """Constructor.""" self._account = account self._vehicle = vehicle @@ -61,6 +65,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): self._unique_id = "{}-{}".format(self._vehicle.vin, self._attribute) self._sensor_name = sensor_name self._device_class = device_class + self._icon = icon self._state = None @property @@ -81,6 +86,11 @@ class BMWConnectedDriveSensor(BinarySensorDevice): """Return the name of the binary sensor.""" return self._name + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + @property def device_class(self): """Return the class of the binary sensor.""" @@ -112,23 +122,19 @@ class BMWConnectedDriveSensor(BinarySensorDevice): for report in vehicle_state.condition_based_services: result.update(self._format_cbs_report(report)) elif self._attribute == "check_control_messages": - check_control_messages = vehicle_state.check_control_messages - if not check_control_messages: - result["check_control_messages"] = "OK" - else: + check_control_messages = vehicle_state.has_check_control_messages + if check_control_messages: cbs_list = [] for message in check_control_messages: cbs_list.append(message["ccmDescriptionShort"]) result["check_control_messages"] = cbs_list + else: + result["check_control_messages"] = "OK" elif self._attribute == "charging_status": result["charging_status"] = vehicle_state.charging_status.value - # pylint: disable=protected-access - result["last_charging_end_result"] = vehicle_state._attributes[ - "lastChargingEndResult" - ] - if self._attribute == "connection_status": - # pylint: disable=protected-access - result["connection_status"] = vehicle_state._attributes["connectionStatus"] + result["last_charging_end_result"] = vehicle_state.last_charging_end_result + elif self._attribute == "connection_status": + result["connection_status"] = vehicle_state.connection_status return sorted(result.items()) @@ -166,8 +172,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): # device class plug: On means device is plugged in, # Off means device is unplugged if self._attribute == "connection_status": - # pylint: disable=protected-access - self._state = vehicle_state._attributes["connectionStatus"] == "CONNECTED" + self._state = vehicle_state.connection_status == "CONNECTED" def _format_cbs_report(self, report): result = {} diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index eec81aa6525..0cc875c50f9 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -1,11 +1,12 @@ { "domain": "bmw_connected_drive", - "name": "Bmw connected drive", + "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/components/bmw_connected_drive", "requirements": [ - "bimmer_connected==0.5.3" + "bimmer_connected==0.6.0" ], "dependencies": [], "codeowners": [ + "@gerard33" ] -} +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index bc133fa4034..8248ded4f8b 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -51,14 +51,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for account in accounts: for vehicle in account.account.vehicles: for attribute_name in vehicle.drive_train_attributes: - device = BMWConnectedDriveSensor( - account, vehicle, attribute_name, attribute_info - ) - devices.append(device) - device = BMWConnectedDriveSensor( - account, vehicle, "mileage", attribute_info - ) - devices.append(device) + if attribute_name in vehicle.available_attributes: + device = BMWConnectedDriveSensor( + account, vehicle, attribute_name, attribute_info + ) + devices.append(device) add_entities(devices, True) diff --git a/requirements_all.txt b/requirements_all.txt index 70378e90fda..bb12466cd71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -266,7 +266,7 @@ beautifulsoup4==4.8.0 bellows-homeassistant==0.9.1 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.5.3 +bimmer_connected==0.6.0 # homeassistant.components.bizkaibus bizkaibus==0.1.1 From f80c9c93cab41215629a408f3f33097cf3141d1e Mon Sep 17 00:00:00 2001 From: SukramJ Date: Wed, 21 Aug 2019 16:31:55 +0200 Subject: [PATCH 224/273] Add support for warning attributes to Homematic IP Cloud (#26103) * add supported optional features * use recommendations --- .../components/homematicip_cloud/device.py | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 6ff39c8b3a7..0fffad8e97e 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -18,6 +18,28 @@ ATTR_RSSI_DEVICE = "rssi_device" ATTR_RSSI_PEER = "rssi_peer" ATTR_SABOTAGE = "sabotage" ATTR_GROUP_MEMBER_UNREACHABLE = "group_member_unreachable" +ATTR_DEVICE_OVERHEATED = "device_overheated" +ATTR_DEVICE_OVERLOADED = "device_overloaded" +ATTR_DEVICE_UNTERVOLTAGE = "device_undervoltage" + +DEVICE_ATTRIBUTE_ICONS = { + "lowBat": "mdi:battery-outline", + "sabotage": "mdi:alert", + "deviceOverheated": "mdi:alert", + "deviceOverloaded": "mdi:alert", + "deviceUndervoltage": "mdi:alert", +} + +DEVICE_ATTRIBUTES = { + "modelType": ATTR_MODEL_TYPE, + "id": ATTR_ID, + "sabotage": ATTR_SABOTAGE, + "rssiDeviceValue": ATTR_RSSI_DEVICE, + "rssiPeerValue": ATTR_RSSI_PEER, + "deviceOverheated": ATTR_DEVICE_OVERHEATED, + "deviceOverloaded": ATTR_DEVICE_OVERLOADED, + "deviceUndervoltage": ATTR_DEVICE_UNTERVOLTAGE, +} class HomematicipGenericDevice(Entity): @@ -85,21 +107,19 @@ class HomematicipGenericDevice(Entity): @property def icon(self) -> Optional[str]: """Return the icon.""" - if hasattr(self._device, "lowBat") and self._device.lowBat: - return "mdi:battery-outline" - if hasattr(self._device, "sabotage") and self._device.sabotage: - return "mdi:alert" + for attr, icon in DEVICE_ATTRIBUTE_ICONS.items(): + if getattr(self._device, attr, None): + return icon + return None @property def device_state_attributes(self): """Return the state attributes of the generic device.""" - attr = {ATTR_MODEL_TYPE: self._device.modelType, ATTR_ID: self._device.id} + state_attr = {} + for attr, attr_key in DEVICE_ATTRIBUTES.items(): + attr_value = getattr(self._device, attr, None) + if attr_value: + state_attr[attr_key] = attr_value - if hasattr(self._device, "sabotage") and self._device.sabotage: - attr[ATTR_SABOTAGE] = self._device.sabotage - if hasattr(self._device, "rssiDeviceValue") and self._device.rssiDeviceValue: - attr[ATTR_RSSI_DEVICE] = self._device.rssiDeviceValue - if hasattr(self._device, "rssiPeerValue") and self._device.rssiPeerValue: - attr[ATTR_RSSI_PEER] = self._device.rssiPeerValue - return attr + return state_attr From cf505c65b44d346ca2866212959d89356a2052d5 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 21 Aug 2019 17:12:31 +0200 Subject: [PATCH 225/273] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 1b547d5c609..81bb1944bed 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -154,6 +154,13 @@ stages: pool: vmImage: 'ubuntu-latest' steps: + - script: | + echo '{ "experimental": true }' | sudo tee /etc/docker/daemon.json + sudo service docker restart + + sleep 15 + sudo docker login -u $(dockerUser) -p $(dockerPassword) + displayName: 'Enable manifest / Docker login' - script: | set -e export DOCKER_CLI_EXPERIMENTAL=enabled From 08d797edba563efec5983a7b506229066d6a1e7a Mon Sep 17 00:00:00 2001 From: croghostrider Date: Wed, 21 Aug 2019 17:42:26 +0200 Subject: [PATCH 226/273] check if a light supports brightness (#26055) Fix black Fix black --- .../components/emulated_hue/hue_api.py | 28 +++++++++++++------ tests/components/emulated_hue/test_hue_api.py | 14 ++++++++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 1b08b43c9af..fc00746fc7f 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -562,17 +562,27 @@ def get_entity_state(config, entity): def entity_to_json(config, entity, state): """Convert an entity to its Hue bridge JSON representation.""" + entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if (entity_features & SUPPORT_BRIGHTNESS) or entity.domain != light.DOMAIN: + return { + "state": { + HUE_API_STATE_ON: state[STATE_ON], + HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], + HUE_API_STATE_HUE: state[STATE_HUE], + HUE_API_STATE_SAT: state[STATE_SATURATION], + "reachable": True, + }, + "type": "Dimmable light", + "name": config.get_entity_name(entity), + "modelid": "HASS123", + "uniqueid": entity.entity_id, + "swversion": "123", + } return { - "state": { - HUE_API_STATE_ON: state[STATE_ON], - HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], - HUE_API_STATE_HUE: state[STATE_HUE], - HUE_API_STATE_SAT: state[STATE_SATURATION], - "reachable": True, - }, - "type": "Dimmable light", + "state": {HUE_API_STATE_ON: state[STATE_ON], "reachable": True}, + "type": "On/off light", "name": config.get_entity_name(entity), - "modelid": "HASS123", + "modelid": "HASS321", "uniqueid": entity.entity_id, "swversion": "123", } diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 57f29a4ef61..02f24f5afba 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -128,6 +128,9 @@ def hass_hue(loop, hass): kitchen_light_entity.entity_id, kitchen_light_entity.state, attributes=attrs ) + # create a lamp without brightness support + hass.states.async_set("light.no_brightness", "on", {}) + # Ceiling Fan is explicitly excluded from being exposed ceiling_fan_entity = hass.states.get("fan.ceiling_fan") attrs = dict(ceiling_fan_entity.attributes) @@ -218,6 +221,17 @@ def test_discover_lights(hue_client): assert "climate.ecobee" not in devices +@asyncio.coroutine +def test_light_without_brightness_supported(hass_hue, hue_client): + """Test that light without brightness is supported.""" + light_without_brightness_json = yield from perform_get_light_state( + hue_client, "light.no_brightness", 200 + ) + + assert light_without_brightness_json["state"][HUE_API_STATE_ON] is True + assert light_without_brightness_json["type"] == "On/off light" + + @asyncio.coroutine def test_get_light_state(hass_hue, hue_client): """Test the getting of light state.""" From 46bfd5e9c832e260565d80bcc76f29c00e47b8b9 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 21 Aug 2019 09:13:04 -0700 Subject: [PATCH 227/273] bump quirks (#26106) --- 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 dcf38e63d2f..0e004893033 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.9.1", - "zha-quirks==0.0.20", + "zha-quirks==0.0.21", "zigpy-deconz==0.2.2", "zigpy-homeassistant==0.7.1", "zigpy-xbee-homeassistant==0.4.0", diff --git a/requirements_all.txt b/requirements_all.txt index bb12466cd71..4a4c77339dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1971,7 +1971,7 @@ zengge==0.2 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.20 +zha-quirks==0.0.21 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 From 39d62b43ac86bc69eca5ac08dee136e508913df8 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Wed, 21 Aug 2019 18:13:40 +0200 Subject: [PATCH 228/273] Upgrade ruamel_yaml to 0.15.100 (#26095) --- 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 2c649b007e3..0dd9b6d7802 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,7 +20,7 @@ python-slugify==3.0.3 pytz>=2019.02 pyyaml==5.1.2 requests==2.22.0 -ruamel.yaml==0.15.99 +ruamel.yaml==0.15.100 sqlalchemy==1.3.7 voluptuous-serialize==2.2.0 voluptuous==0.11.7 diff --git a/requirements_all.txt b/requirements_all.txt index 4a4c77339dc..17a707dedf8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -15,7 +15,7 @@ python-slugify==3.0.3 pytz>=2019.02 pyyaml==5.1.2 requests==2.22.0 -ruamel.yaml==0.15.99 +ruamel.yaml==0.15.100 voluptuous==0.11.7 voluptuous-serialize==2.2.0 diff --git a/setup.py b/setup.py index 01288c01bf1..5ab8d74c64c 100755 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ REQUIRES = [ "pytz>=2019.02", "pyyaml==5.1.2", "requests==2.22.0", - "ruamel.yaml==0.15.99", + "ruamel.yaml==0.15.100", "voluptuous==0.11.7", "voluptuous-serialize==2.2.0", ] From 8f044cf52f491e0a1b92e7d4b6e5f5940c089e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 21 Aug 2019 19:48:46 +0300 Subject: [PATCH 229/273] Upgrade pydocstyle to 4.0.1 (#26111) https://github.com/PyCQA/pydocstyle/blob/4.0.1/docs/release_notes.rst#401---august-14th-2019 --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 77162b55d7b..5dbb89ddcf4 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,7 +12,7 @@ flake8==3.7.8 mock-open==1.3.1 mypy==0.720 pre-commit==1.18.2 -pydocstyle==4.0.0 +pydocstyle==4.0.1 pylint==2.3.1 pytest-aiohttp==0.3.0 pytest-cov==2.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5380f53c979..5169b5bb56f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -13,7 +13,7 @@ flake8==3.7.8 mock-open==1.3.1 mypy==0.720 pre-commit==1.18.2 -pydocstyle==4.0.0 +pydocstyle==4.0.1 pylint==2.3.1 pytest-aiohttp==0.3.0 pytest-cov==2.7.1 From 48e9e53f66fd23ce035d01849c5e0d5e098cd347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 21 Aug 2019 19:49:25 +0300 Subject: [PATCH 230/273] Upgrade pytest to 5.1.1 (#26112) https://docs.pytest.org/en/latest/changelog.html#pytest-5-1-0-2019-08-15 https://docs.pytest.org/en/latest/changelog.html#pytest-5-1-1-2019-08-20 --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 5dbb89ddcf4..ef18759d0e8 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -18,5 +18,5 @@ pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.0.1 +pytest==5.1.1 requests_mock==1.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5169b5bb56f..23262689c0c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.0.1 +pytest==5.1.1 requests_mock==1.6.0 From e033e46161cbcc69cba787c58f2d2d6e74d4b1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 21 Aug 2019 19:49:46 +0300 Subject: [PATCH 231/273] Remove coveralls test dependency (#26110) Outdated and unused. --- requirements_test.txt | 1 - requirements_test_all.txt | 1 - 2 files changed, 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index ef18759d0e8..bfe459b0cfb 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,6 @@ asynctest==0.13.0 black==19.3b0 codecov==2.0.15 -coveralls==1.2.0 flake8-docstrings==1.3.1 flake8==3.7.8 mock-open==1.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 23262689c0c..24f383d81cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,6 @@ asynctest==0.13.0 black==19.3b0 codecov==2.0.15 -coveralls==1.2.0 flake8-docstrings==1.3.1 flake8==3.7.8 mock-open==1.3.1 From 9b1315d8e55f0ca906c4c8a1b2ae8c2ea511dc90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 21 Aug 2019 19:50:26 +0300 Subject: [PATCH 232/273] Check and fix test suite leaving files behind (#25981) * azure: run check_dirty at end of tests * Fix ps4 media player tests to not write to files * .gitignore coverage.xml and test-results.xml --- .gitignore | 2 + azure-pipelines-ci.yml | 2 + tests/components/ps4/test_media_player.py | 47 ++++++++++++++++------- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 65b325a0a4b..85e66ce829c 100644 --- a/.gitignore +++ b/.gitignore @@ -59,9 +59,11 @@ pip-log.txt # Unit test / coverage reports .coverage .tox +coverage.xml nosetests.xml htmlcov/ test-reports/ +test-results.xml # Translations *.mo diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 5297fd80231..0ee272f900d 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -114,6 +114,7 @@ stages: - script: | . venv/bin/activate pytest --timeout=9 --durations=10 --junitxml=test-results.xml -qq -o console_output_style=count -p no:sugar tests + script/check_dirty displayName: 'Run pytest for python $(python.container)' condition: and(succeeded(), ne(variables['python.container'], variables['PythonMain'])) - script: | @@ -122,6 +123,7 @@ stages: . venv/bin/activate pytest --timeout=9 --durations=10 --junitxml=test-results.xml --cov --cov-report=xml -qq -o console_output_style=count -p no:sugar tests codecov --token $(codecovToken) + script/check_dirty displayName: 'Run pytest for python $(python.container) / coverage' condition: and(succeeded(), eq(variables['python.container'], variables['PythonMain'])) - task: PublishTestResults@2 diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index f7e2f865cb2..e4f2033c3cb 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -192,7 +192,8 @@ async def test_state_off_is_set(hass): """Test that state is set to off.""" mock_entity_id = await setup_mock_component(hass) - await mock_ddp_response(hass, MOCK_STATUS_OFF) + with patch(MOCK_SAVE, side_effect=MagicMock()): + await mock_ddp_response(hass, MOCK_STATUS_OFF) assert hass.states.get(mock_entity_id).state == STATE_OFF @@ -217,7 +218,8 @@ async def test_state_idle_is_set(hass): """Test that state is set to idle.""" mock_entity_id = await setup_mock_component(hass) - await mock_ddp_response(hass, MOCK_STATUS_IDLE) + with patch(MOCK_SAVE, side_effect=MagicMock()): + await mock_ddp_response(hass, MOCK_STATUS_IDLE) assert hass.states.get(mock_entity_id).state == STATE_IDLE @@ -246,7 +248,6 @@ async def test_media_attributes_are_fetched(hass): with patch(mock_func, return_value=mock_coro(mock_result)) as mock_fetch, patch( MOCK_SAVE, side_effect=MagicMock() ): - await mock_ddp_response(hass, MOCK_STATUS_PLAYING) mock_state = hass.states.get(mock_entity_id) @@ -271,7 +272,9 @@ async def test_media_attributes_are_loaded(hass): "pyps4.Ps4Async.async_get_ps_store_data", ) - with patch(mock_func, return_value=mock_coro(None)) as mock_fetch: + with patch(mock_func, return_value=mock_coro(None)) as mock_fetch, patch( + MOCK_SAVE, side_effect=MagicMock() + ): await mock_ddp_response(hass, MOCK_STATUS_PLAYING, mock_data) mock_state = hass.states.get(mock_entity_id) @@ -292,7 +295,9 @@ async def test_media_attributes_are_loaded(hass): async def test_device_info_is_set_from_status_correctly(hass): """Test that device info is set correctly from status update.""" mock_d_registry = mock_device_registry(hass) - with patch("pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_OFF): + with patch( + "pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_OFF + ), patch(MOCK_SAVE, side_effect=MagicMock()): mock_entity_id = await setup_mock_component(hass) await hass.async_block_till_done() @@ -374,7 +379,9 @@ async def test_turn_on(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.wakeup" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): await hass.services.async_call( "media_player", "turn_on", {ATTR_ENTITY_ID: mock_entity_id} ) @@ -390,7 +397,9 @@ async def test_turn_off(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.standby" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): await hass.services.async_call( "media_player", "turn_off", {ATTR_ENTITY_ID: mock_entity_id} ) @@ -406,7 +415,9 @@ async def test_media_pause(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.remote_control" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): await hass.services.async_call( "media_player", "media_pause", {ATTR_ENTITY_ID: mock_entity_id} ) @@ -422,7 +433,9 @@ async def test_media_stop(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.remote_control" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): await hass.services.async_call( "media_player", "media_stop", {ATTR_ENTITY_ID: mock_entity_id} ) @@ -443,7 +456,9 @@ async def test_select_source(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.start_title" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): # Test with title name. await hass.services.async_call( "media_player", @@ -467,7 +482,9 @@ async def test_select_source_caps(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.start_title" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): # Test with title name in caps. await hass.services.async_call( "media_player", @@ -494,7 +511,9 @@ async def test_select_source_id(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.start_title" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): # Test with title ID. await hass.services.async_call( "media_player", @@ -513,7 +532,9 @@ async def test_ps4_send_command(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.remote_control" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): await hass.services.async_call( DOMAIN, "send_command", {ATTR_ENTITY_ID: mock_entity_id, ATTR_COMMAND: "ps"} ) From ce54ae31712cdb72a8ec52bf2d8dd80e1198ea16 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Wed, 21 Aug 2019 19:58:03 +0200 Subject: [PATCH 233/273] Update PyEssent (#26115) --- homeassistant/components/essent/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/essent/manifest.json b/homeassistant/components/essent/manifest.json index 41313cb44a9..aeb3b48311e 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.12"], + "requirements": ["PyEssent==0.13"], "dependencies": [], "codeowners": ["@TheLastProject"] } diff --git a/requirements_all.txt b/requirements_all.txt index 17a707dedf8..e6172fe4fa6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -44,7 +44,7 @@ Mastodon.py==1.4.6 OPi.GPIO==0.3.6 # homeassistant.components.essent -PyEssent==0.12 +PyEssent==0.13 # homeassistant.components.github PyGithub==1.43.5 From 95f660f0b4b8c225a5c07c04a176f7f3da496a6c Mon Sep 17 00:00:00 2001 From: SukramJ Date: Wed, 21 Aug 2019 19:58:34 +0200 Subject: [PATCH 234/273] remove ATTR_ID for groups (#26114) --- homeassistant/components/homematicip_cloud/binary_sensor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 7bb7718f0b3..8ecbfeab01a 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -38,7 +38,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_GROUP_MEMBER_UNREACHABLE +from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_ID _LOGGER = logging.getLogger(__name__) @@ -311,6 +311,10 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorD """Return the state attributes of the security zone group.""" attr = super().device_state_attributes + # Remove ATTR_ID from dict, because security groups don't have + # device id/sgtin, just an ugly uuid that is referenced no where else. + del attr[ATTR_ID] + if self._device.motionDetected: attr[ATTR_MOTIONDETECTED] = True if self._device.presenceDetected: From 7ab36e03813ad1c9c7594748da6afb0b299c7062 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 21 Aug 2019 20:54:20 +0200 Subject: [PATCH 235/273] Update eternalegypt to 0.0.10 (#26117) --- CODEOWNERS | 1 + homeassistant/components/netgear_lte/manifest.json | 6 ++++-- requirements_all.txt | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 7a153b3ff82..81c5aafed30 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -180,6 +180,7 @@ homeassistant/components/nello/* @pschmitt homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/nest/* @awarecan homeassistant/components/netdata/* @fabaff +homeassistant/components/netgear_lte/* @amelchio homeassistant/components/nextbus/* @vividboarder homeassistant/components/nissan_leaf/* @filcole homeassistant/components/nmbs/* @thibmaek diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index 8f5db991c76..609ea72cc69 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -3,8 +3,10 @@ "name": "Netgear lte", "documentation": "https://www.home-assistant.io/components/netgear_lte", "requirements": [ - "eternalegypt==0.0.9" + "eternalegypt==0.0.10" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@amelchio" + ] } diff --git a/requirements_all.txt b/requirements_all.txt index e6172fe4fa6..1984b1bb478 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -455,7 +455,7 @@ epson-projector==0.1.3 epsonprinter==0.0.9 # homeassistant.components.netgear_lte -eternalegypt==0.0.9 +eternalegypt==0.0.10 # homeassistant.components.keyboard_remote # evdev==0.6.1 From 588eac82c725c7e03470f706392d80f1c410ffa6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 21 Aug 2019 22:22:42 +0200 Subject: [PATCH 236/273] UniFi config entry options (#26113) Introduce config entry options for Unifi integration Allow configuration.yaml options to be imported to new options --- .../components/unifi/.translations/en.json | 43 +++++++---- homeassistant/components/unifi/__init__.py | 10 +-- homeassistant/components/unifi/config_flow.py | 71 ++++++++++++++++- homeassistant/components/unifi/const.py | 13 +++- homeassistant/components/unifi/controller.py | 76 ++++++++++++++++++- .../components/unifi/device_tracker.py | 36 +++------ homeassistant/components/unifi/strings.json | 15 ++++ homeassistant/components/unifi/switch.py | 2 +- tests/components/unifi/test_controller.py | 19 ++++- tests/components/unifi/test_device_tracker.py | 24 +++--- tests/components/unifi/test_init.py | 3 +- tests/components/unifi/test_switch.py | 13 ++-- 12 files changed, 250 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/unifi/.translations/en.json b/homeassistant/components/unifi/.translations/en.json index 3686148fdb6..c484bfbf09f 100644 --- a/homeassistant/components/unifi/.translations/en.json +++ b/homeassistant/components/unifi/.translations/en.json @@ -1,26 +1,41 @@ { "config": { - "abort": { - "already_configured": "Controller site is already configured", - "user_privilege": "User needs to be administrator" + "title": "UniFi Controller", + "step": { + "user": { + "title": "Set up UniFi Controller", + "data": { + "host": "Host", + "username": "User name", + "password": "Password", + "port": "Port", + "site": "Site ID", + "verify_ssl": "Controller using proper certificate" + } + } }, "error": { "faulty_credentials": "Bad user credentials", "service_unavailable": "No service available" }, + "abort": { + "already_configured": "Controller site is already configured", + "user_privilege": "User needs to be administrator" + } + }, + "options": { "step": { - "user": { + "init": { + "data": {} + }, + "device_tracker": { "data": { - "host": "Host", - "password": "Password", - "port": "Port", - "site": "Site ID", - "username": "User name", - "verify_ssl": "Controller using proper certificate" - }, - "title": "Set up UniFi Controller" + "detection_time": "Time in seconds from last seen until considered away", + "track_clients": "Track network clients", + "track_devices": "Track network devices (Ubiquiti devices)", + "track_wired_clients": "Include wired network clients" + } } - }, - "title": "UniFi Controller" + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 4ca6f68c301..da9bbb8e59e 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -11,9 +11,6 @@ from .const import ( CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, - CONF_DONT_TRACK_CLIENTS, - CONF_DONT_TRACK_DEVICES, - CONF_DONT_TRACK_WIRED_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, CONTROLLER_ID, @@ -23,6 +20,9 @@ from .const import ( from .controller import UniFiController CONF_CONTROLLERS = "controllers" +CONF_DONT_TRACK_CLIENTS = "dont_track_clients" +CONF_DONT_TRACK_DEVICES = "dont_track_devices" +CONF_DONT_TRACK_WIRED_CLIENTS = "dont_track_wired_clients" CONTROLLER_SCHEMA = vol.Schema( { @@ -34,9 +34,7 @@ CONTROLLER_SCHEMA = vol.Schema( vol.Optional(CONF_DONT_TRACK_CLIENTS): cv.boolean, vol.Optional(CONF_DONT_TRACK_DEVICES): cv.boolean, vol.Optional(CONF_DONT_TRACK_WIRED_CLIENTS): cv.boolean, - vol.Optional(CONF_DETECTION_TIME): vol.All( - cv.time_period, cv.positive_timedelta - ), + vol.Optional(CONF_DETECTION_TIME): cv.positive_int, vol.Optional(CONF_SSID_FILTER): vol.All(cv.ensure_list, [cv.string]), } ) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index e5a8965dff9..e1f0a91c774 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -2,6 +2,7 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.core import callback from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -10,7 +11,20 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) -from .const import CONF_CONTROLLER, CONF_SITE_ID, DOMAIN, LOGGER +from .const import ( + CONF_CONTROLLER, + CONF_TRACK_CLIENTS, + CONF_TRACK_DEVICES, + CONF_TRACK_WIRED_CLIENTS, + CONF_DETECTION_TIME, + CONF_SITE_ID, + DEFAULT_TRACK_CLIENTS, + DEFAULT_TRACK_DEVICES, + DEFAULT_TRACK_WIRED_CLIENTS, + DEFAULT_DETECTION_TIME, + DOMAIN, + LOGGER, +) from .controller import get_controller from .errors import AlreadyConfigured, AuthenticationRequired, CannotConnect @@ -26,6 +40,12 @@ class UnifiFlowHandler(config_entries.ConfigFlow): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return UnifiOptionsFlowHandler(config_entry) + def __init__(self): """Initialize the UniFi flow.""" self.config = None @@ -142,3 +162,52 @@ class UnifiFlowHandler(config_entries.ConfigFlow): self.desc = import_config[CONF_SITE_ID] return await self.async_step_user(user_input=config) + + +class UnifiOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Unifi options.""" + + def __init__(self, config_entry): + """Initialize UniFi options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the UniFi options.""" + return await self.async_step_device_tracker() + + async def async_step_device_tracker(self, user_input=None): + """Manage the device tracker options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="device_tracker", + data_schema=vol.Schema( + { + vol.Optional( + CONF_TRACK_CLIENTS, + default=self.config_entry.options.get( + CONF_TRACK_CLIENTS, DEFAULT_TRACK_CLIENTS + ), + ): bool, + vol.Optional( + CONF_TRACK_WIRED_CLIENTS, + default=self.config_entry.options.get( + CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS + ), + ): bool, + vol.Optional( + CONF_TRACK_DEVICES, + default=self.config_entry.options.get( + CONF_TRACK_DEVICES, DEFAULT_TRACK_DEVICES + ), + ): bool, + vol.Optional( + CONF_DETECTION_TIME, + default=self.config_entry.options.get( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ), + ): int, + } + ), + ) diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index b4864421cb9..ffa9a28818b 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -13,9 +13,16 @@ UNIFI_CONFIG = "unifi_config" CONF_BLOCK_CLIENT = "block_client" CONF_DETECTION_TIME = "detection_time" -CONF_DONT_TRACK_CLIENTS = "dont_track_clients" -CONF_DONT_TRACK_DEVICES = "dont_track_devices" -CONF_DONT_TRACK_WIRED_CLIENTS = "dont_track_wired_clients" +CONF_TRACK_CLIENTS = "track_clients" +CONF_TRACK_DEVICES = "track_devices" +CONF_TRACK_WIRED_CLIENTS = "track_wired_clients" CONF_SSID_FILTER = "ssid_filter" +DEFAULT_BLOCK_CLIENTS = [] +DEFAULT_TRACK_CLIENTS = True +DEFAULT_TRACK_DEVICES = True +DEFAULT_TRACK_WIRED_CLIENTS = True +DEFAULT_DETECTION_TIME = 300 +DEFAULT_SSID_FILTER = [] + ATTR_MANUFACTURER = "Ubiquiti Networks" diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index cb82e6cf1c1..47c692b12b2 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -1,4 +1,6 @@ """UniFi Controller abstraction.""" +from datetime import timedelta + import asyncio import ssl import async_timeout @@ -15,8 +17,19 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( CONF_BLOCK_CLIENT, CONF_CONTROLLER, + CONF_DETECTION_TIME, + CONF_TRACK_CLIENTS, + CONF_TRACK_DEVICES, + CONF_TRACK_WIRED_CLIENTS, CONF_SITE_ID, + CONF_SSID_FILTER, CONTROLLER_ID, + DEFAULT_BLOCK_CLIENTS, + DEFAULT_TRACK_CLIENTS, + DEFAULT_TRACK_DEVICES, + DEFAULT_TRACK_WIRED_CLIENTS, + DEFAULT_DETECTION_TIME, + DEFAULT_SSID_FILTER, LOGGER, UNIFI_CONFIG, ) @@ -59,9 +72,40 @@ class UniFiController: return self._site_role @property - def block_clients(self): - """Return list of clients to block.""" - return self.unifi_config.get(CONF_BLOCK_CLIENT, []) + def option_block_clients(self): + """Config entry option with list of clients to control network access.""" + return self.config_entry.options.get(CONF_BLOCK_CLIENT, DEFAULT_BLOCK_CLIENTS) + + @property + def option_track_clients(self): + """Config entry option to not track clients.""" + return self.config_entry.options.get(CONF_TRACK_CLIENTS, DEFAULT_TRACK_CLIENTS) + + @property + def option_track_devices(self): + """Config entry option to not track devices.""" + return self.config_entry.options.get(CONF_TRACK_DEVICES, DEFAULT_TRACK_DEVICES) + + @property + def option_track_wired_clients(self): + """Config entry option to not track wired clients.""" + return self.config_entry.options.get( + CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS + ) + + @property + def option_detection_time(self): + """Config entry option defining number of seconds from last seen to away.""" + return timedelta( + seconds=self.config_entry.options.get( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ) + ) + + @property + def option_ssid_filter(self): + """Config entry option listing what SSIDs are being used to track clients.""" + return self.config_entry.options.get(CONF_SSID_FILTER, DEFAULT_SSID_FILTER) @property def mac(self): @@ -96,7 +140,7 @@ class UniFiController: with async_timeout.timeout(10): await self.api.clients.update() await self.api.devices.update() - if self.block_clients: + if self.option_block_clients: await self.api.clients_all.update() except aiounifi.LoginRequired: @@ -155,6 +199,30 @@ class UniFiController: self.unifi_config = unifi_config break + options = dict(self.config_entry.options) + + if CONF_BLOCK_CLIENT in self.unifi_config: + options[CONF_BLOCK_CLIENT] = self.unifi_config[CONF_BLOCK_CLIENT] + + if CONF_TRACK_CLIENTS in self.unifi_config: + options[CONF_TRACK_CLIENTS] = self.unifi_config[CONF_TRACK_CLIENTS] + + if CONF_TRACK_DEVICES in self.unifi_config: + options[CONF_TRACK_DEVICES] = self.unifi_config[CONF_TRACK_DEVICES] + + if CONF_TRACK_WIRED_CLIENTS in self.unifi_config: + options[CONF_TRACK_WIRED_CLIENTS] = self.unifi_config[ + CONF_TRACK_WIRED_CLIENTS + ] + + if CONF_DETECTION_TIME in self.unifi_config: + options[CONF_DETECTION_TIME] = self.unifi_config[CONF_DETECTION_TIME] + + if CONF_SSID_FILTER in self.unifi_config: + options[CONF_SSID_FILTER] = self.unifi_config[CONF_SSID_FILTER] + + hass.config_entries.async_update_entry(self.config_entry, options=options) + for platform in ["device_tracker", "switch"]: hass.async_create_task( hass.config_entries.async_forward_entry_setup( diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 491a032e1cc..c8024808e39 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -27,12 +27,7 @@ import homeassistant.util.dt as dt_util from .const import ( ATTR_MANUFACTURER, CONF_CONTROLLER, - CONF_DETECTION_TIME, - CONF_DONT_TRACK_CLIENTS, - CONF_DONT_TRACK_DEVICES, - CONF_DONT_TRACK_WIRED_CLIENTS, CONF_SITE_ID, - CONF_SSID_FILTER, CONTROLLER_ID, DOMAIN as UNIFI_DOMAIN, ) @@ -151,11 +146,11 @@ def update_items(controller, async_add_entities, tracked): """Update tracked device state from the controller.""" new_tracked = [] - if not controller.unifi_config.get(CONF_DONT_TRACK_CLIENTS, False): + if controller.option_track_clients: for client_id in controller.api.clients: - if client_id in tracked: + if client_id in tracked and tracked[client_id].entity_id: LOGGER.debug( "Updating UniFi tracked client %s (%s)", tracked[client_id].entity_id, @@ -168,15 +163,12 @@ def update_items(controller, async_add_entities, tracked): if ( not client.is_wired - and CONF_SSID_FILTER in controller.unifi_config - and client.essid not in controller.unifi_config[CONF_SSID_FILTER] + and controller.option_ssid_filter + and client.essid not in controller.option_ssid_filter ): continue - if ( - controller.unifi_config.get(CONF_DONT_TRACK_WIRED_CLIENTS, False) - and client.is_wired - ): + if not controller.option_track_wired_clients and client.is_wired: continue tracked[client_id] = UniFiClientTracker(client, controller) @@ -187,11 +179,11 @@ def update_items(controller, async_add_entities, tracked): client.mac, ) - if not controller.unifi_config.get(CONF_DONT_TRACK_DEVICES, False): + if controller.option_track_devices: for device_id in controller.api.devices: - if device_id in tracked: + if device_id in tracked and tracked[device_id].entity_id: LOGGER.debug( "Updating UniFi tracked device %s (%s)", tracked[device_id].entity_id, @@ -229,14 +221,11 @@ class UniFiClientTracker(ScannerEntity): @property def is_connected(self): """Return true if the client is connected to the network.""" - detection_time = self.controller.unifi_config.get( - CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME - ) - if ( dt_util.utcnow() - dt_util.utc_from_timestamp(float(self.client.last_seen)) - ) < detection_time: + ) < self.controller.option_detection_time: return True + return False @property @@ -291,15 +280,12 @@ class UniFiDeviceTracker(ScannerEntity): @property def is_connected(self): """Return true if the device is connected to the network.""" - detection_time = self.controller.unifi_config.get( - CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME - ) - if self.device.state == 1 and ( dt_util.utcnow() - dt_util.utc_from_timestamp(float(self.device.last_seen)) - < detection_time + < self.controller.option_detection_time ): return True + return False @property diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index 938ac058d22..c484bfbf09f 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -22,5 +22,20 @@ "already_configured": "Controller site is already configured", "user_privilege": "User needs to be administrator" } + }, + "options": { + "step": { + "init": { + "data": {} + }, + "device_tracker": { + "data": { + "detection_time": "Time in seconds from last seen until considered away", + "track_clients": "Track network clients", + "track_devices": "Track network devices (Ubiquiti devices)", + "track_wired_clients": "Include wired network clients" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 2b7965d1095..b7bb9b730ad 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -74,7 +74,7 @@ def update_items(controller, async_add_entities, switches, switches_off): devices = controller.api.devices # block client - for client_id in controller.block_clients: + for client_id in controller.option_block_clients: block_client_id = "block-{}".format(client_id) diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index f92eebabd20..714db8604b2 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -37,9 +37,23 @@ ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} async def test_controller_setup(): """Successful setup.""" hass = Mock() - hass.data = {UNIFI_CONFIG: {}} + hass.data = { + UNIFI_CONFIG: [ + { + CONF_HOST: CONTROLLER_DATA[CONF_HOST], + CONF_SITE_ID: "nice name", + controller.CONF_BLOCK_CLIENT: [], + controller.CONF_TRACK_CLIENTS: True, + controller.CONF_TRACK_DEVICES: True, + controller.CONF_TRACK_WIRED_CLIENTS: True, + controller.CONF_DETECTION_TIME: 300, + controller.CONF_SSID_FILTER: [], + } + ] + } entry = Mock() entry.data = ENTRY_CONFIG + entry.options = {} api = Mock() api.initialize.return_value = mock_coro(True) api.sites.return_value = mock_coro(CONTROLLER_SITES) @@ -89,6 +103,7 @@ async def test_controller_mac(): hass.data = {UNIFI_CONFIG: {}} entry = Mock() entry.data = ENTRY_CONFIG + entry.options = {} client = Mock() client.ip = "1.2.3.4" client.mac = "00:11:22:33:44:55" @@ -111,6 +126,7 @@ async def test_controller_no_mac(): hass.data = {UNIFI_CONFIG: {}} entry = Mock() entry.data = ENTRY_CONFIG + entry.options = {} client = Mock() client.ip = "5.6.7.8" api = Mock() @@ -182,6 +198,7 @@ async def test_reset_unloads_entry_if_setup(): hass.data = {UNIFI_CONFIG: {}} entry = Mock() entry.data = ENTRY_CONFIG + entry.options = {} api = Mock() api.initialize.return_value = mock_coro(True) api.sites.return_value = mock_coro(CONTROLLER_SITES) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 0da72c924c4..30c2191625e 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -14,6 +14,7 @@ from homeassistant.components import unifi from homeassistant.components.unifi.const import ( CONF_CONTROLLER, CONF_SITE_ID, + CONF_SSID_FILTER, UNIFI_CONFIG, ) from homeassistant.const import ( @@ -133,7 +134,7 @@ def mock_controller(hass): return controller -async def setup_controller(hass, mock_controller): +async def setup_controller(hass, mock_controller, options={}): """Load the UniFi switch platform with the provided controller.""" hass.config.components.add(unifi.DOMAIN) hass.data[unifi.DOMAIN] = {CONTROLLER_ID: mock_controller} @@ -146,6 +147,7 @@ async def setup_controller(hass, mock_controller): config_entries.CONN_CLASS_LOCAL_POLL, entry_id=1, system_options={}, + options=options, ) mock_controller.config_entry = config_entry @@ -182,9 +184,9 @@ async def test_tracked_devices(hass, mock_controller): """Test the update_items function with some clients.""" mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2, CLIENT_3]) mock_controller.mock_device_responses.append([DEVICE_1, DEVICE_2]) - mock_controller.unifi_config = {unifi_dt.CONF_SSID_FILTER: ["ssid"]} + options = {CONF_SSID_FILTER: ["ssid"]} - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 2 assert len(hass.states.async_all()) == 5 @@ -234,7 +236,7 @@ async def test_restoring_client(hass, mock_controller): mock_controller.mock_client_responses.append([CLIENT_2]) mock_controller.mock_device_responses.append({}) mock_controller.mock_client_all_responses.append([CLIENT_1]) - mock_controller.unifi_config = {unifi.CONF_BLOCK_CLIENT: True} + options = {unifi.CONF_BLOCK_CLIENT: True} config_entry = config_entries.ConfigEntry( 1, @@ -263,7 +265,7 @@ async def test_restoring_client(hass, mock_controller): config_entry=config_entry, ) - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 3 assert len(hass.states.async_all()) == 4 @@ -275,9 +277,9 @@ async def test_dont_track_clients(hass, mock_controller): """Test dont track clients config works.""" mock_controller.mock_client_responses.append([CLIENT_1]) mock_controller.mock_device_responses.append([DEVICE_1]) - mock_controller.unifi_config = {unifi.CONF_DONT_TRACK_CLIENTS: True} + options = {unifi.controller.CONF_TRACK_CLIENTS: False} - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 2 assert len(hass.states.async_all()) == 3 @@ -293,9 +295,9 @@ async def test_dont_track_devices(hass, mock_controller): """Test dont track devices config works.""" mock_controller.mock_client_responses.append([CLIENT_1]) mock_controller.mock_device_responses.append([DEVICE_1]) - mock_controller.unifi_config = {unifi.CONF_DONT_TRACK_DEVICES: True} + options = {unifi.controller.CONF_TRACK_DEVICES: False} - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 2 assert len(hass.states.async_all()) == 3 @@ -311,9 +313,9 @@ async def test_dont_track_wired_clients(hass, mock_controller): """Test dont track wired clients config works.""" mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2]) mock_controller.mock_device_responses.append({}) - mock_controller.unifi_config = {unifi.CONF_DONT_TRACK_WIRED_CLIENTS: True} + options = {unifi.controller.CONF_TRACK_WIRED_CLIENTS: False} - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 2 assert len(hass.states.async_all()) == 3 diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index c7f87579c08..b725e34f61d 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -1,5 +1,4 @@ """Test UniFi setup process.""" -from datetime import timedelta from unittest.mock import Mock, patch from homeassistant.components import unifi @@ -44,7 +43,7 @@ async def test_setup_with_config(hass): unifi.CONF_HOST: "1.2.3.4", unifi.CONF_SITE_ID: "My site", unifi.CONF_BLOCK_CLIENT: ["12:34:56:78:90:AB"], - unifi.CONF_DETECTION_TIME: timedelta(seconds=3), + unifi.CONF_DETECTION_TIME: 3, unifi.CONF_SSID_FILTER: ["ssid"], } ] diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 05c58abbc94..3ac9ddb17dc 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -250,7 +250,7 @@ def mock_controller(hass): return controller -async def setup_controller(hass, mock_controller): +async def setup_controller(hass, mock_controller, options={}): """Load the UniFi switch platform with the provided controller.""" hass.config.components.add(unifi.DOMAIN) hass.data[unifi.DOMAIN] = {CONTROLLER_ID: mock_controller} @@ -263,6 +263,7 @@ async def setup_controller(hass, mock_controller): config_entries.CONN_CLASS_LOCAL_POLL, entry_id=1, system_options={}, + options=options, ) mock_controller.config_entry = config_entry @@ -320,11 +321,9 @@ async def test_switches(hass, mock_controller): mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_4]) mock_controller.mock_device_responses.append([DEVICE_1]) mock_controller.mock_client_all_responses.append([BLOCKED, UNBLOCKED, CLIENT_1]) - mock_controller.unifi_config = { - unifi.CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]] - } + options = {unifi.CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]]} - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 3 assert len(hass.states.async_all()) == 5 @@ -467,7 +466,7 @@ async def test_restoring_client(hass, mock_controller): mock_controller.mock_client_responses.append([CLIENT_2]) mock_controller.mock_device_responses.append([DEVICE_1]) mock_controller.mock_client_all_responses.append([CLIENT_1]) - mock_controller.unifi_config = {unifi.CONF_BLOCK_CLIENT: ["random mac"]} + options = {unifi.CONF_BLOCK_CLIENT: ["random mac"]} config_entry = config_entries.ConfigEntry( 1, @@ -496,7 +495,7 @@ async def test_restoring_client(hass, mock_controller): config_entry=config_entry, ) - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 3 assert len(hass.states.async_all()) == 3 From 9bcb48985be9d9498ed37b1c54cd2b57cc8ca307 Mon Sep 17 00:00:00 2001 From: Steven Rollason <2099542+gadgetchnnel@users.noreply.github.com> Date: Wed, 21 Aug 2019 22:07:27 +0100 Subject: [PATCH 237/273] Template binary sensor attributes (#22664) * Added attribute support to template binary sensor with tests Added attribute support to template binary sensor with tests * fix dictionary update fix dictionary update * Fixed whitespace and line length issues * Fixed indentation * Simplify applying of attribute templates based on feedback * Syntax and whitespace fixes * Black formatting * Black formatting on tests * Check attribute_templates is not None * Fixed test * Added test for failure to render template * Test fix * Updated test * Removed whitespace and applied Black formatting * Fixed test assertion * Updated test * Code improvements folloing comments Using chain to iterate over templates and attribute_templates Replacing dict() with {} Rmoving unused constant * Applied Black formatting * Fixed removed code * Default attribute_templates to empty dict * Black formatting * Fixed imports --- .../components/template/binary_sensor.py | 46 ++++++++--- .../components/template/test_binary_sensor.py | 82 ++++++++++++++++++- 2 files changed, 117 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 8b354f4eeb2..e0fc8677200 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -1,5 +1,6 @@ """Support for exposing a templated binary sensor.""" import logging +from itertools import chain import voluptuous as vol @@ -30,12 +31,14 @@ _LOGGER = logging.getLogger(__name__) CONF_DELAY_ON = "delay_on" CONF_DELAY_OFF = "delay_off" +CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" SENSOR_SCHEMA = vol.Schema( { vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, + vol.Optional(CONF_ATTRIBUTE_TEMPLATES): vol.Schema({cv.string: cv.template}), vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, @@ -59,14 +62,17 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) entity_ids = set() manual_entity_ids = device_config.get(ATTR_ENTITY_ID) + attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {}) invalid_templates = [] - for tpl_name, template in ( - (CONF_VALUE_TEMPLATE, value_template), - (CONF_ICON_TEMPLATE, icon_template), - (CONF_ENTITY_PICTURE_TEMPLATE, entity_picture_template), - ): + templates = { + CONF_VALUE_TEMPLATE: value_template, + CONF_ICON_TEMPLATE: icon_template, + CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, + } + + for tpl_name, template in chain(templates.items(), attribute_templates.items()): if template is None: continue template.hass = hass @@ -78,7 +84,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if template_entity_ids == MATCH_ALL: entity_ids = MATCH_ALL # Cut off _template from name - invalid_templates.append(tpl_name[:-9]) + invalid_templates.append(tpl_name.replace("_template", "")) elif entity_ids != MATCH_ALL: entity_ids |= set(template_entity_ids) @@ -114,6 +120,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= entity_ids, delay_on, delay_off, + attribute_templates, ) ) if not sensors: @@ -139,6 +146,7 @@ class BinarySensorTemplate(BinarySensorDevice): entity_ids, delay_on, delay_off, + attribute_templates, ): """Initialize the Template binary sensor.""" self.hass = hass @@ -154,6 +162,8 @@ class BinarySensorTemplate(BinarySensorDevice): self._entities = entity_ids self._delay_on = delay_on self._delay_off = delay_off + self._attribute_templates = attribute_templates + self._attributes = {} async def async_added_to_hass(self): """Register callbacks.""" @@ -203,6 +213,11 @@ class BinarySensorTemplate(BinarySensorDevice): """Return the sensor class of the sensor.""" return self._device_class + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + @property def should_poll(self): """No polling needed.""" @@ -225,10 +240,21 @@ class BinarySensorTemplate(BinarySensorDevice): return _LOGGER.error("Could not render template %s: %s", self._name, ex) - for property_name, template in ( - ("_icon", self._icon_template), - ("_entity_picture", self._entity_picture_template), - ): + templates = { + "_icon": self._icon_template, + "_entity_picture": self._entity_picture_template, + } + + attrs = {} + if self._attribute_templates is not None: + for key, value in self._attribute_templates.items(): + try: + attrs[key] = value.async_render() + except TemplateError as err: + _LOGGER.error("Error rendering attribute %s: %s", key, err) + self._attributes = attrs + + for property_name, template in templates.items(): if template is None: continue diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index c0b73f9c559..c8cec168d6e 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -166,6 +166,38 @@ class TestBinarySensorTemplate(unittest.TestCase): state = self.hass.states.get("binary_sensor.test_template_sensor") assert state.attributes["entity_picture"] == "/local/sensor.png" + def test_attribute_templates(self): + """Test attribute_templates template.""" + with assert_setup_component(1): + assert setup.setup_component( + self.hass, + "binary_sensor", + { + "binary_sensor": { + "platform": "template", + "sensors": { + "test_template_sensor": { + "value_template": "{{ states.sensor.xyz.state }}", + "attribute_templates": { + "test_attribute": "It {{ states.sensor.test_state.state }}." + }, + } + }, + } + }, + ) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get("binary_sensor.test_template_sensor") + assert state.attributes.get("test_attribute") == "It ." + + self.hass.states.set("sensor.test_state", "Works") + self.hass.block_till_done() + state = self.hass.states.get("binary_sensor.test_template_sensor") + assert state.attributes["test_attribute"] == "It Works." + @mock.patch( "homeassistant.components.template.binary_sensor." "BinarySensorTemplate._async_render" @@ -209,6 +241,7 @@ class TestBinarySensorTemplate(unittest.TestCase): MATCH_ALL, None, None, + None, ).result() assert not vs.should_poll assert "motion" == vs.device_class @@ -268,6 +301,7 @@ class TestBinarySensorTemplate(unittest.TestCase): MATCH_ALL, None, None, + None, ).result() mock_render.side_effect = TemplateError("foo") run_callback_threadsafe(self.hass.loop, vs.async_check_state).result() @@ -394,6 +428,36 @@ async def test_template_delay_off(hass): assert state.state == "on" +async def test_invalid_attribute_template(hass, caplog): + """Test that errors are logged if rendering template fails.""" + hass.states.async_set("binary_sensor.test_sensor", "true") + + await setup.async_setup_component( + hass, + "binary_sensor", + { + "binary_sensor": { + "platform": "template", + "sensors": { + "invalid_template": { + "value_template": "{{ states.binary_sensor.test_sensor }}", + "attribute_templates": { + "test_attribute": "{{ states.binary_sensor.unknown.attributes.picture }}" + }, + } + }, + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + await hass.helpers.entity_component.async_update_entity( + "binary_sensor.invalid_template" + ) + + assert ("Error rendering attribute test_attribute") in caplog.text + + async def test_no_update_template_match_all(hass, caplog): """Test that we do not update sensors that match on all.""" hass.states.async_set("binary_sensor.test_sensor", "true") @@ -414,12 +478,16 @@ async def test_no_update_template_match_all(hass, caplog): "value_template": "{{ states.binary_sensor.test_sensor.state }}", "entity_picture_template": "{{ 1 + 1 }}", }, + "all_attribute": { + "value_template": "{{ states.binary_sensor.test_sensor.state }}", + "attribute_templates": {"test_attribute": "{{ 1 + 1 }}"}, + }, }, } }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 assert ( "Template binary sensor all_state has no entity ids " "configured to track nor were we able to extract the entities to " @@ -435,10 +503,16 @@ async def test_no_update_template_match_all(hass, caplog): "configured to track nor were we able to extract the entities to " "track from the entity_picture template" ) in caplog.text + assert ( + "Template binary sensor all_attribute has no entity ids " + "configured to track nor were we able to extract the entities to " + "track from the test_attribute template" + ) in caplog.text assert hass.states.get("binary_sensor.all_state").state == "off" assert hass.states.get("binary_sensor.all_icon").state == "off" assert hass.states.get("binary_sensor.all_entity_picture").state == "off" + assert hass.states.get("binary_sensor.all_attribute").state == "off" hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -446,6 +520,7 @@ async def test_no_update_template_match_all(hass, caplog): assert hass.states.get("binary_sensor.all_state").state == "on" assert hass.states.get("binary_sensor.all_icon").state == "on" assert hass.states.get("binary_sensor.all_entity_picture").state == "on" + assert hass.states.get("binary_sensor.all_attribute").state == "on" hass.states.async_set("binary_sensor.test_sensor", "false") await hass.async_block_till_done() @@ -453,13 +528,18 @@ async def test_no_update_template_match_all(hass, caplog): assert hass.states.get("binary_sensor.all_state").state == "on" assert hass.states.get("binary_sensor.all_icon").state == "on" assert hass.states.get("binary_sensor.all_entity_picture").state == "on" + assert hass.states.get("binary_sensor.all_attribute").state == "on" await hass.helpers.entity_component.async_update_entity("binary_sensor.all_state") await hass.helpers.entity_component.async_update_entity("binary_sensor.all_icon") await hass.helpers.entity_component.async_update_entity( "binary_sensor.all_entity_picture" ) + await hass.helpers.entity_component.async_update_entity( + "binary_sensor.all_attribute" + ) assert hass.states.get("binary_sensor.all_state").state == "on" assert hass.states.get("binary_sensor.all_icon").state == "off" assert hass.states.get("binary_sensor.all_entity_picture").state == "off" + assert hass.states.get("binary_sensor.all_attribute").state == "off" From 709097043697794a46377690737db65ff9f2f271 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 21 Aug 2019 14:08:46 -0700 Subject: [PATCH 238/273] Add descriptive fields to script config (#26056) * Add descriptive fields to script config * Add script descriptions to hass.data["service_description_cache"] * Import SERVICE_DESCRIPTION_CACHE * Register script descriptions via async_set_service_schema * Add scripts test for loading and reloading service descriptions * Minor cleanup * Clean up script schema --- homeassistant/components/script/__init__.py | 23 +++++- homeassistant/helpers/service.py | 14 ++++ tests/components/script/test_init.py | 92 +++++++++++++++++++++ 3 files changed, 128 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 44e11d83afa..d810d50cfbf 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -20,6 +20,7 @@ from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA +from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.script import Script @@ -31,6 +32,9 @@ ATTR_LAST_ACTION = "last_action" ATTR_LAST_TRIGGERED = "last_triggered" ATTR_VARIABLES = "variables" +CONF_DESCRIPTION = "description" +CONF_EXAMPLE = "example" +CONF_FIELDS = "fields" CONF_SEQUENCE = "sequence" ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -38,7 +42,17 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" GROUP_NAME_ALL_SCRIPTS = "all scripts" SCRIPT_ENTRY_SCHEMA = vol.Schema( - {CONF_ALIAS: cv.string, vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA} + { + CONF_ALIAS: cv.string, + vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_DESCRIPTION, default=""): cv.string, + vol.Optional(CONF_FIELDS, default={}): { + cv.string: { + vol.Optional(CONF_DESCRIPTION): cv.string, + vol.Optional(CONF_EXAMPLE): cv.string, + } + }, + } ) CONFIG_SCHEMA = vol.Schema( @@ -137,6 +151,13 @@ async def _async_process_config(hass, config, component): DOMAIN, object_id, service_handler, schema=SCRIPT_SERVICE_SCHEMA ) + # Register the service description + service_desc = { + CONF_DESCRIPTION: cfg[CONF_DESCRIPTION], + CONF_FIELDS: cfg[CONF_FIELDS], + } + async_set_service_schema(hass, DOMAIN, object_id, service_desc) + await component.async_add_entities(scripts) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 07e070df8c5..f29d1885d1e 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -231,6 +231,20 @@ async def async_get_all_descriptions(hass): return descriptions +@ha.callback +@bind_hass +def async_set_service_schema(hass, domain, service, schema): + """Register a description for a service.""" + hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {}) + + description = { + "description": schema.get("description") or "", + "fields": schema.get("fields") or {}, + } + + hass.data[SERVICE_DESCRIPTION_CACHE]["{}.{}".format(domain, service)] = description + + @bind_hass async def entity_service_call( hass, platforms, func, call, service_name="", required_features=None diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 7be682eff5e..d675034e744 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -17,6 +17,7 @@ from homeassistant.const import ( EVENT_SCRIPT_STARTED, ) from homeassistant.core import Context, callback, split_entity_id +from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import bind_hass from homeassistant.setup import setup_component, async_setup_component from homeassistant.exceptions import ServiceNotFound @@ -244,6 +245,61 @@ class TestScriptComponent(unittest.TestCase): assert self.hass.services.has_service(script.DOMAIN, "test2") +async def test_service_descriptions(hass): + """Test that service descriptions are loaded and reloaded correctly.""" + # Test 1: has "description" but no "fields" + assert await async_setup_component( + hass, + "script", + { + "script": { + "test": { + "description": "test description", + "sequence": [{"delay": {"seconds": 5}}], + } + } + }, + ) + + descriptions = await async_get_all_descriptions(hass) + + assert descriptions[DOMAIN]["test"]["description"] == "test description" + assert not descriptions[DOMAIN]["test"]["fields"] + + # Test 2: has "fields" but no "description" + await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) + with patch( + "homeassistant.config.load_yaml_config_file", + return_value={ + "script": { + "test": { + "fields": { + "test_param": { + "description": "test_param description", + "example": "test_param example", + } + }, + "sequence": [{"delay": {"seconds": 5}}], + } + } + }, + ): + with patch("homeassistant.config.find_config_file", return_value=""): + await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) + + descriptions = await async_get_all_descriptions(hass) + + assert descriptions[script.DOMAIN]["test"]["description"] == "" + assert ( + descriptions[script.DOMAIN]["test"]["fields"]["test_param"]["description"] + == "test_param description" + ) + assert ( + descriptions[script.DOMAIN]["test"]["fields"]["test_param"]["example"] + == "test_param example" + ) + + async def test_shared_context(hass): """Test that the shared context is passed down the chain.""" event = "test_event" @@ -306,3 +362,39 @@ async def test_turning_no_scripts_off(hass): await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {"entity_id": []}, blocking=True ) + + +async def test_async_get_descriptions_script(hass): + """Test async_set_service_schema for the script integration.""" + script = hass.components.script + script_config = { + script.DOMAIN: { + "test1": {"sequence": [{"service": "homeassistant.restart"}]}, + "test2": { + "description": "test2", + "fields": { + "param": { + "description": "param_description", + "example": "param_example", + } + }, + "sequence": [{"service": "homeassistant.restart"}], + }, + } + } + + await async_setup_component(hass, script.DOMAIN, script_config) + descriptions = await hass.helpers.service.async_get_all_descriptions() + + assert descriptions[script.DOMAIN]["test1"]["description"] == "" + assert not descriptions[script.DOMAIN]["test1"]["fields"] + + assert descriptions[script.DOMAIN]["test2"]["description"] == "test2" + assert ( + descriptions[script.DOMAIN]["test2"]["fields"]["param"]["description"] + == "param_description" + ) + assert ( + descriptions[script.DOMAIN]["test2"]["fields"]["param"]["example"] + == "param_example" + ) From 4a6f722b6d8c928c89fb2856028d5cade968745f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 Aug 2019 15:16:34 -0700 Subject: [PATCH 239/273] Updated frontend to 20190821.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 2337c3cb469..648fc8b96df 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==20190820.1" + "home-assistant-frontend==20190821.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0dd9b6d7802..b26e1c7e59f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.17 -home-assistant-frontend==20190820.1 +home-assistant-frontend==20190821.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1984b1bb478..33d6be841cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -624,7 +624,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190820.1 +home-assistant-frontend==20190821.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24f383d81cc..9d254f72e9b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -176,7 +176,7 @@ hdate==0.9.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190820.1 +home-assistant-frontend==20190821.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From f9e518d868cb112b866ee3256cd355aba31b3c49 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 Aug 2019 15:53:10 -0700 Subject: [PATCH 240/273] Fix tests leaving files behind (#26121) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 85e66ce829c..5389954ca59 100644 --- a/.gitignore +++ b/.gitignore @@ -124,3 +124,6 @@ desktop.ini # monkeytype monkeytype.sqlite3 + +# This is left behind by Azure Restore Cache +tmp_cache From 12f964ca57f2e56dbb4a9cfb8411a36ac14466f9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 22 Aug 2019 00:54:04 +0200 Subject: [PATCH 241/273] Statistics sensors repect given name (#26119) --- homeassistant/components/statistics/sensor.py | 5 +---- tests/components/statistics/test_sensor.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 252a29591c9..51868c6d0a8 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -82,10 +82,7 @@ class StatisticsSensor(Entity): """Initialize the Statistics sensor.""" self._entity_id = entity_id self.is_binary = self._entity_id.split(".")[0] == "binary_sensor" - if not self.is_binary: - self._name = "{} {}".format(name, ATTR_MEAN) - else: - self._name = "{} {}".format(name, ATTR_COUNT) + self._name = name self._sampling_size = sampling_size self._max_age = max_age self._precision = precision diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 32aa2d56558..2a28876f552 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -60,7 +60,7 @@ class TestStatisticsSensor(unittest.TestCase): self.hass.states.set("binary_sensor.test_monitored", value) self.hass.block_till_done() - state = self.hass.states.get("sensor.test_count") + state = self.hass.states.get("sensor.test") assert str(len(values)) == state.state @@ -87,7 +87,7 @@ class TestStatisticsSensor(unittest.TestCase): ) self.hass.block_till_done() - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") assert str(self.mean) == state.state assert self.min == state.attributes.get("min_value") @@ -126,7 +126,7 @@ class TestStatisticsSensor(unittest.TestCase): ) self.hass.block_till_done() - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") assert 3.8 == state.attributes.get("min_value") assert 14 == state.attributes.get("max_value") @@ -155,7 +155,7 @@ class TestStatisticsSensor(unittest.TestCase): ) self.hass.block_till_done() - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") # require only one data point assert self.values[-1] == state.attributes.get("min_value") @@ -206,7 +206,7 @@ class TestStatisticsSensor(unittest.TestCase): # insert the next value one minute later mock_data["return_time"] += timedelta(minutes=1) - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") assert 6 == state.attributes.get("min_value") assert 14 == state.attributes.get("max_value") @@ -248,7 +248,7 @@ class TestStatisticsSensor(unittest.TestCase): # insert the next value one minute later mock_data["return_time"] += timedelta(minutes=1) - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") assert datetime( 2017, 8, 2, 12, 23, 42, tzinfo=dt_util.UTC @@ -290,7 +290,7 @@ class TestStatisticsSensor(unittest.TestCase): self.hass.block_till_done() # check if the result is as in test_sensor_source() - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") assert str(self.mean) == state.state @pytest.mark.skip("Flaky in CI") @@ -355,7 +355,7 @@ class TestStatisticsSensor(unittest.TestCase): self.hass.block_till_done() # check if the result is as in test_sensor_source() - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") assert expected_min_age == state.attributes.get("min_age") # The max_age timestamp should be 1 hour before what we have right From 23cf8414b8de51d86719498c78b996b5ce203f1f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 Aug 2019 16:20:08 -0700 Subject: [PATCH 242/273] Bumped version to 0.98.0b0 --- homeassistant/const.py | 2 +- script/version_bump.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index eebd10f4fb9..aebcb95c3b1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 98 -PATCH_VERSION = "0.dev0" +PATCH_VERSION = "0b0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) diff --git a/script/version_bump.py b/script/version_bump.py index 7c584daae7e..db3f3ac273d 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -102,7 +102,7 @@ def write_version(version): "MINOR_VERSION = .*\n", "MINOR_VERSION = {}\n".format(minor), content ) content = re.sub( - "PATCH_VERSION = .*\n", "PATCH_VERSION = '{}'\n".format(patch), content + "PATCH_VERSION = .*\n", 'PATCH_VERSION = "{}"\n'.format(patch), content ) with open("homeassistant/const.py", "wt") as fil: From e53ecfb5d533a44f995484e27a7f58b6e2e99120 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 22 Aug 2019 08:58:41 +0200 Subject: [PATCH 243/273] Update azure-pipelines-release.yml for Azure Pipelines (#26128) * Update azure-pipelines-release.yml for Azure Pipelines * Update azure-pipelines-release.yml --- azure-pipelines-release.yml | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 81bb1944bed..d0cfc294db1 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -7,7 +7,7 @@ trigger: pr: none variables: - name: versionBuilder - value: '5.2' + value: '6.1' - group: docker - group: github - group: twine @@ -155,48 +155,46 @@ stages: vmImage: 'ubuntu-latest' steps: - script: | - echo '{ "experimental": true }' | sudo tee /etc/docker/daemon.json - sudo service docker restart + mkdir -p ~/.docker + echo '{ "experimental": "enabled" }' > .docker/config.json - sleep 15 sudo docker login -u $(dockerUser) -p $(dockerPassword) displayName: 'Enable manifest / Docker login' - script: | set -e - export DOCKER_CLI_EXPERIMENTAL=enabled function create_manifest() { local tag_l=$1 local tag_r=$2 - sudo docker manifest create homeassistant/home-assistant:${tag_l} \ + sudo docker --config .docker manifest create homeassistant/home-assistant:${tag_l} \ homeassistant/amd64-homeassistant:${tag_r} \ homeassistant/i386-homeassistant:${tag_r} \ homeassistant/armhf-homeassistant:${tag_r} \ homeassistant/armv7-homeassistant:${tag_r} \ homeassistant/aarch64-homeassistant:${tag_r} - sudo docker manifest annotate homeassistant/home-assistant:${tag_l} \ + sudo docker --config .docker manifest annotate homeassistant/home-assistant:${tag_l} \ homeassistant/amd64-homeassistant:${tag_r} \ --os linux --arch amd64 - sudo docker manifest annotate homeassistant/home-assistant:${tag_l} \ + sudo docker --config .docker manifest annotate homeassistant/home-assistant:${tag_l} \ homeassistant/i386-homeassistant:${tag_r} \ --os linux --arch i386 - sudo docker manifest annotate homeassistant/home-assistant:${tag_l} \ + sudo docker --config .docker manifest annotate homeassistant/home-assistant:${tag_l} \ homeassistant/armhf-homeassistant:${tag_r} \ --os linux --arch arm --variant=v6 - sudo docker manifest annotate homeassistant/home-assistant:${tag_l} \ + sudo docker --config .docker manifest annotate homeassistant/home-assistant:${tag_l} \ homeassistant/armv7-homeassistant:${tag_r} \ --os linux --arch arm --variant=v7 - sudo docker manifest annotate homeassistant/home-assistant:${tag_l} \ + sudo docker --config .docker manifest annotate homeassistant/home-assistant:${tag_l} \ homeassistant/aarch64-homeassistant:${tag_r} \ --os linux --arch arm64 --variant=v8 - sudo docker manifest push --purge homeassistant/home-assistant:${tag_l} + sudo docker --config .docker manifest push --purge homeassistant/home-assistant:${tag_l} } # Create version tag From a71a02926216f7c94d8fbd90943430ee8095595c Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 22 Aug 2019 09:29:03 +0200 Subject: [PATCH 244/273] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index d0cfc294db1..7409be5f98c 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -180,7 +180,7 @@ stages: sudo docker --config .docker manifest annotate homeassistant/home-assistant:${tag_l} \ homeassistant/i386-homeassistant:${tag_r} \ - --os linux --arch i386 + --os linux --arch 386 sudo docker --config .docker manifest annotate homeassistant/home-assistant:${tag_l} \ homeassistant/armhf-homeassistant:${tag_r} \ From 44a528dee2e563d8b22400378f785dee7387e449 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 22 Aug 2019 17:35:22 +0200 Subject: [PATCH 245/273] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 7409be5f98c..44d910a8106 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -197,6 +197,12 @@ stages: sudo docker --config .docker manifest push --purge homeassistant/home-assistant:${tag_l} } + sudo docker pull homeassistant/amd64-homeassistant:$(Build.SourceBranchName) + sudo docker pull homeassistant/i368-homeassistant:$(Build.SourceBranchName) + sudo docker pull homeassistant/armhf-homeassistant:$(Build.SourceBranchName) + sudo docker pull homeassistant/armv7-homeassistant:$(Build.SourceBranchName) + sudo docker pull homeassistant/aarch64-homeassistant:$(Build.SourceBranchName) + # Create version tag create_manifest "$(Build.SourceBranchName)" "$(Build.SourceBranchName)" @@ -205,6 +211,7 @@ stages: create_manifest "dev" "$(Build.SourceBranchName)" elif [[ "$version" =~ b ]]; then create_manifest "beta" "$(Build.SourceBranchName)" + create_manifest "rc" "$(Build.SourceBranchName)" else create_manifest "stable" "$(Build.SourceBranchName)" create_manifest "latest" "$(Build.SourceBranchName)" From 82e5a384039a64c97e388313c663434b8009a7e4 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 22 Aug 2019 17:47:04 +0200 Subject: [PATCH 246/273] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 44d910a8106..2ad13288e08 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -215,6 +215,7 @@ stages: else create_manifest "stable" "$(Build.SourceBranchName)" create_manifest "latest" "$(Build.SourceBranchName)" + create_manifest "beta" "$(Build.SourceBranchName)" fi displayName: 'Create Meta-Image' From 5f8c3e623546e7e1b0cbef5bd377115befd59bd9 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 22 Aug 2019 17:50:51 +0200 Subject: [PATCH 247/273] Update azure-pipelines-release.yml --- azure-pipelines-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 2ad13288e08..6b986329291 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -216,6 +216,7 @@ stages: create_manifest "stable" "$(Build.SourceBranchName)" create_manifest "latest" "$(Build.SourceBranchName)" create_manifest "beta" "$(Build.SourceBranchName)" + create_manifest "rc" "$(Build.SourceBranchName)" fi displayName: 'Create Meta-Image' From 8856a1cda6e8965177ac904d62a185e92d26425c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 22 Aug 2019 15:05:57 -0700 Subject: [PATCH 248/273] Updated frontend to 20190822.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 648fc8b96df..8d6271183bd 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==20190821.0" + "home-assistant-frontend==20190822.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b26e1c7e59f..0f4fb56970b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.17 -home-assistant-frontend==20190821.0 +home-assistant-frontend==20190822.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 33d6be841cf..f5484829de7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -624,7 +624,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190821.0 +home-assistant-frontend==20190822.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9d254f72e9b..b5d139719ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -176,7 +176,7 @@ hdate==0.9.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190821.0 +home-assistant-frontend==20190822.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 49bc3d3769c0e779f74a0fed8128a2ef24fa2287 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 22 Aug 2019 11:01:56 -0700 Subject: [PATCH 249/273] Load user-provided descriptions for python_scripts (#26069) * Load user-provided descriptions for python_scripts * Import SERVICE_DESCRIPTION_CACHE * Use async_set_service_schema to register service descriptions * Add python_script tests for loading service descriptions * Use async/await in test --- .../components/python_script/__init__.py | 15 +++ tests/components/python_script/test_init.py | 100 +++++++++++++++++- 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index 788da6a8d64..715c06aca43 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -9,8 +9,10 @@ import voluptuous as vol from homeassistant.const import SERVICE_RELOAD from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.service import async_set_service_schema from homeassistant.loader import bind_hass from homeassistant.util import sanitize_filename +from homeassistant.util.yaml.loader import load_yaml import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -90,10 +92,23 @@ def discover_scripts(hass): continue hass.services.remove(DOMAIN, existing_service) + # Load user-provided service descriptions from python_scripts/services.yaml + services_yaml = os.path.join(path, "services.yaml") + if os.path.exists(services_yaml): + services_dict = load_yaml(services_yaml) + else: + services_dict = {} + for fil in glob.iglob(os.path.join(path, "*.py")): name = os.path.splitext(os.path.basename(fil))[0] hass.services.register(DOMAIN, name, python_script_service_handler) + service_desc = { + "description": services_dict.get(name, {}).get("description", ""), + "fields": services_dict.get(name, {}).get("fields", {}), + } + async_set_service_schema(hass, DOMAIN, name, service_desc) + @bind_hass def execute_script(hass, name, data=None): diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index fcf1519d4c7..d7732c00f94 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -3,8 +3,11 @@ import asyncio import logging from unittest.mock import patch, mock_open +from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.setup import async_setup_component -from homeassistant.components.python_script import execute +from homeassistant.components.python_script import DOMAIN, execute, FOLDER + +from tests.common import patch_yaml_files @asyncio.coroutine @@ -289,6 +292,101 @@ def test_reload(hass): assert hass.services.has_service("python_script", "reload") +async def test_service_descriptions(hass): + """Test that service descriptions are loaded and reloaded correctly.""" + # Test 1: no user-provided services.yaml file + scripts1 = [ + "/some/config/dir/python_scripts/hello.py", + "/some/config/dir/python_scripts/world_beer.py", + ] + + service_descriptions1 = ( + "hello:\n" + " description: Description of hello.py.\n" + " fields:\n" + " fake_param:\n" + " description: Parameter used by hello.py.\n" + " example: 'This is a test of python_script.hello'" + ) + services_yaml1 = { + "{}/{}/services.yaml".format( + hass.config.config_dir, FOLDER + ): service_descriptions1 + } + + with patch( + "homeassistant.components.python_script.os.path.isdir", return_value=True + ), patch( + "homeassistant.components.python_script.glob.iglob", return_value=scripts1 + ), patch( + "homeassistant.components.python_script.os.path.exists", return_value=True + ), patch_yaml_files( + services_yaml1 + ): + await async_setup_component(hass, DOMAIN, {}) + + descriptions = await async_get_all_descriptions(hass) + + assert len(descriptions) == 1 + + assert descriptions[DOMAIN]["hello"]["description"] == "Description of hello.py." + assert ( + descriptions[DOMAIN]["hello"]["fields"]["fake_param"]["description"] + == "Parameter used by hello.py." + ) + assert ( + descriptions[DOMAIN]["hello"]["fields"]["fake_param"]["example"] + == "This is a test of python_script.hello" + ) + + assert descriptions[DOMAIN]["world_beer"]["description"] == "" + assert bool(descriptions[DOMAIN]["world_beer"]["fields"]) is False + + # Test 2: user-provided services.yaml file + scripts2 = [ + "/some/config/dir/python_scripts/hello2.py", + "/some/config/dir/python_scripts/world_beer.py", + ] + + service_descriptions2 = ( + "hello2:\n" + " description: Description of hello2.py.\n" + " fields:\n" + " fake_param:\n" + " description: Parameter used by hello2.py.\n" + " example: 'This is a test of python_script.hello2'" + ) + services_yaml2 = { + "{}/{}/services.yaml".format( + hass.config.config_dir, FOLDER + ): service_descriptions2 + } + + with patch( + "homeassistant.components.python_script.os.path.isdir", return_value=True + ), patch( + "homeassistant.components.python_script.glob.iglob", return_value=scripts2 + ), patch( + "homeassistant.components.python_script.os.path.exists", return_value=True + ), patch_yaml_files( + services_yaml2 + ): + await hass.services.async_call(DOMAIN, "reload", {}, blocking=True) + descriptions = await async_get_all_descriptions(hass) + + assert len(descriptions) == 1 + + assert descriptions[DOMAIN]["hello2"]["description"] == "Description of hello2.py." + assert ( + descriptions[DOMAIN]["hello2"]["fields"]["fake_param"]["description"] + == "Parameter used by hello2.py." + ) + assert ( + descriptions[DOMAIN]["hello2"]["fields"]["fake_param"]["example"] + == "This is a test of python_script.hello2" + ) + + @asyncio.coroutine def test_sleep_warns_one(hass, caplog): """Test time.sleep warns once.""" From 08471e3e52a017ac59e5e73376a34b91732d5463 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Thu, 22 Aug 2019 18:02:35 +0200 Subject: [PATCH 250/273] Splitt device_state_attributes between device and group for Homematic IP Cloud (#26137) * splitt device_state_attributes between device and group * readd device_state_attributes for access point --- .../components/homematicip_cloud/binary_sensor.py | 8 ++------ homeassistant/components/homematicip_cloud/device.py | 9 +++++---- homeassistant/components/homematicip_cloud/sensor.py | 6 ++++++ 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 8ecbfeab01a..97746f3f472 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -38,7 +38,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_ID +from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_MODEL_TYPE _LOGGER = logging.getLogger(__name__) @@ -309,11 +309,7 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorD @property def device_state_attributes(self): """Return the state attributes of the security zone group.""" - attr = super().device_state_attributes - - # Remove ATTR_ID from dict, because security groups don't have - # device id/sgtin, just an ugly uuid that is referenced no where else. - del attr[ATTR_ID] + attr = {ATTR_MODEL_TYPE: self._device.modelType} if self._device.motionDetected: attr[ATTR_MOTIONDETECTED] = True diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 0fffad8e97e..b086eaa29c7 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -117,9 +117,10 @@ class HomematicipGenericDevice(Entity): def device_state_attributes(self): """Return the state attributes of the generic device.""" state_attr = {} - for attr, attr_key in DEVICE_ATTRIBUTES.items(): - attr_value = getattr(self._device, attr, None) - if attr_value: - state_attr[attr_key] = attr_value + if isinstance(self._device, AsyncDevice): + for attr, attr_key in DEVICE_ATTRIBUTES.items(): + attr_value = getattr(self._device, attr, None) + if attr_value: + state_attr[attr_key] = attr_value return state_attr diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index add03c6b644..c15b3121d3a 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -34,6 +34,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice +from .device import ATTR_MODEL_TYPE _LOGGER = logging.getLogger(__name__) @@ -142,6 +143,11 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): """Return the unit this state is expressed in.""" return "%" + @property + def device_state_attributes(self): + """Return the state attributes of the security zone group.""" + return {ATTR_MODEL_TYPE: self._device.modelType} + class HomematicipHeatingThermostat(HomematicipGenericDevice): """Representation of a HomematicIP heating thermostat device.""" From a58211062993c1a669876e3ebf7e764f74603cba Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Thu, 22 Aug 2019 20:40:48 +0100 Subject: [PATCH 251/273] Nissanleaf login fix (#26139) * Upgrade to pycarwings2.9 per 25 July 2019 API change * Remove rest of location tracker. Fix get_status_from_update call. --- .../components/nissan_leaf/__init__.py | 87 ++++--------------- .../components/nissan_leaf/device_tracker.py | 46 ---------- .../components/nissan_leaf/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 21 insertions(+), 116 deletions(-) delete mode 100644 homeassistant/components/nissan_leaf/device_tracker.py diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 409b4d38208..38b7018af6c 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -24,14 +24,12 @@ DOMAIN = "nissan_leaf" DATA_LEAF = "nissan_leaf_data" DATA_BATTERY = "battery" -DATA_LOCATION = "location" DATA_CHARGING = "charging" DATA_PLUGGED_IN = "plugged_in" DATA_CLIMATE = "climate" DATA_RANGE_AC = "range_ac_on" DATA_RANGE_AC_OFF = "range_ac_off" -CONF_NCONNECT = "nissan_connect" CONF_INTERVAL = "update_interval" CONF_CHARGING_INTERVAL = "update_interval_charging" CONF_CLIMATE_INTERVAL = "update_interval_climate" @@ -61,7 +59,6 @@ CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_REGION): vol.In(CONF_VALID_REGIONS), - vol.Optional(CONF_NCONNECT, default=True): cv.boolean, vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): ( vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL)) ), @@ -84,7 +81,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -LEAF_COMPONENTS = ["sensor", "switch", "binary_sensor", "device_tracker"] +LEAF_COMPONENTS = ["sensor", "switch", "binary_sensor"] SIGNAL_UPDATE_LEAF = "nissan_leaf_update" @@ -177,8 +174,7 @@ def setup(hass, config): hass.data[DATA_LEAF][leaf.vin] = data_store for component in LEAF_COMPONENTS: - if component != "device_tracker" or car_config[CONF_NCONNECT]: - load_platform(hass, component, DOMAIN, {}, car_config) + load_platform(hass, component, DOMAIN, {}, car_config) async_track_point_in_utc_time( hass, data_store.async_update_data, utcnow() + INITIAL_UPDATE @@ -209,24 +205,20 @@ class LeafDataStore: self.hass = hass self.leaf = leaf self.car_config = car_config - self.nissan_connect = car_config[CONF_NCONNECT] self.force_miles = car_config[CONF_FORCE_MILES] self.data = {} self.data[DATA_CLIMATE] = False self.data[DATA_BATTERY] = 0 self.data[DATA_CHARGING] = False - self.data[DATA_LOCATION] = False self.data[DATA_RANGE_AC] = 0 self.data[DATA_RANGE_AC_OFF] = 0 self.data[DATA_PLUGGED_IN] = False self.next_update = None self.last_check = None self.request_in_progress = False - # Timestamp of last successful response from battery, - # climate or location. + # Timestamp of last successful response from battery or climate. self.last_battery_response = None self.last_climate_response = None - self.last_location_response = None self._remove_listener = None async def async_update_data(self, now): @@ -334,20 +326,6 @@ class LeafDataStore: except CarwingsError: _LOGGER.error("Error fetching climate info") - if self.nissan_connect: - try: - location_response = await self.async_get_location() - - if location_response is None: - _LOGGER.debug("Empty Location Response Received") - self.data[DATA_LOCATION] = None - else: - _LOGGER.debug("Location Response: %s", location_response.__dict__) - self.data[DATA_LOCATION] = location_response - self.last_location_response = utcnow() - except CarwingsError: - _LOGGER.error("Error fetching location info") - self.request_in_progress = False async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) @@ -364,19 +342,6 @@ class LeafDataStore: from pycarwings2 import CarwingsError try: - # First, check nissan servers for the latest data - start_server_info = await self.hass.async_add_executor_job( - self.leaf.get_latest_battery_status - ) - - # Store the date from the nissan servers - start_date = self._extract_start_date(start_server_info) - if start_date is None: - _LOGGER.info("No start date from servers. Aborting") - return None - - _LOGGER.debug("Start server date=%s", start_date) - # Request battery update from the car _LOGGER.debug("Requesting battery update, %s", self.leaf.vin) request = await self.hass.async_add_executor_job(self.leaf.request_update) @@ -393,21 +358,30 @@ class LeafDataStore: ) await asyncio.sleep(PYCARWINGS2_SLEEP) - # Note leaf.get_status_from_update is always returning 0, so - # don't try to use it anymore. - server_info = await self.hass.async_add_executor_job( - self.leaf.get_latest_battery_status + # We don't use the response from get_status_from_update + # apart from knowing that the car has responded saying it + # has given the latest battery status to Nissan. + check_result_info = await self.hass.async_add_executor_job( + self.leaf.get_status_from_update, request ) - latest_date = self._extract_start_date(server_info) - _LOGGER.debug("Latest server date=%s", latest_date) - if latest_date is not None and latest_date != start_date: + if check_result_info is not None: + # Get the latest battery status from Nissan servers. + # This has the SOC in it. + server_info = await self.hass.async_add_executor_job( + self.leaf.get_latest_battery_status + ) return server_info _LOGGER.debug( "%s attempts exceeded return latest data from server", MAX_RESPONSE_ATTEMPTS, ) + # Get the latest data from the nissan servers, even though + # it may be out of date, it's better than nothing. + server_info = await self.hass.async_add_executor_job( + self.leaf.get_latest_battery_status + ) return server_info except CarwingsError: _LOGGER.error("An error occurred getting battery status.") @@ -465,29 +439,6 @@ class LeafDataStore: _LOGGER.debug("Climate result not returned by Nissan servers") return False - async def async_get_location(self): - """Get location from Nissan servers.""" - request = await self.hass.async_add_executor_job(self.leaf.request_location) - for attempt in range(MAX_RESPONSE_ATTEMPTS): - if attempt > 0: - _LOGGER.debug( - "Location data not in yet. (%s) (%s). " "Waiting %s seconds", - self.leaf.vin, - attempt, - PYCARWINGS2_SLEEP, - ) - await asyncio.sleep(PYCARWINGS2_SLEEP) - - location_status = await self.hass.async_add_executor_job( - self.leaf.get_status_from_location, request - ) - - if location_status is not None: - _LOGGER.debug("Location_status=%s", location_status.__dict__) - break - - return location_status - class LeafEntity(Entity): """Base class for Nissan Leaf entity.""" diff --git a/homeassistant/components/nissan_leaf/device_tracker.py b/homeassistant/components/nissan_leaf/device_tracker.py deleted file mode 100644 index 11d18ee5a8e..00000000000 --- a/homeassistant/components/nissan_leaf/device_tracker.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Support for tracking a Nissan Leaf.""" -import logging - -from homeassistant.helpers.dispatcher import dispatcher_connect -from homeassistant.util import slugify - -from . import DATA_LEAF, DATA_LOCATION, SIGNAL_UPDATE_LEAF - -_LOGGER = logging.getLogger(__name__) - -ICON_CAR = "mdi:car" - - -def setup_scanner(hass, config, see, discovery_info=None): - """Set up the Nissan Leaf tracker.""" - if discovery_info is None: - return False - - def see_vehicle(): - """Handle the reporting of the vehicle position.""" - for vin, datastore in hass.data[DATA_LEAF].items(): - host_name = datastore.leaf.nickname - dev_id = "nissan_leaf_{}".format(slugify(host_name)) - if not datastore.data[DATA_LOCATION]: - _LOGGER.debug("No position found for vehicle %s", vin) - return - _LOGGER.debug( - "Updating device_tracker for %s with position %s", - datastore.leaf.nickname, - datastore.data[DATA_LOCATION].__dict__, - ) - attrs = {"updated_on": datastore.last_location_response} - see( - dev_id=dev_id, - host_name=host_name, - gps=( - datastore.data[DATA_LOCATION].latitude, - datastore.data[DATA_LOCATION].longitude, - ), - attributes=attrs, - icon=ICON_CAR, - ) - - dispatcher_connect(hass, SIGNAL_UPDATE_LEAF, see_vehicle) - - return True diff --git a/homeassistant/components/nissan_leaf/manifest.json b/homeassistant/components/nissan_leaf/manifest.json index ab94c01b7c1..70aaa112414 100644 --- a/homeassistant/components/nissan_leaf/manifest.json +++ b/homeassistant/components/nissan_leaf/manifest.json @@ -3,7 +3,7 @@ "name": "Nissan leaf", "documentation": "https://www.home-assistant.io/components/nissan_leaf", "requirements": [ - "pycarwings2==2.8" + "pycarwings2==2.9" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index f5484829de7..a2b91f7eb7d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1071,7 +1071,7 @@ pyblackbird==0.5 pybotvac==0.0.15 # homeassistant.components.nissan_leaf -pycarwings2==2.8 +pycarwings2==2.9 # homeassistant.components.cloudflare pycfdns==0.0.1 From 7b62516e693de5ace5834553179c096d56032fff Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 22 Aug 2019 14:12:24 -0700 Subject: [PATCH 252/273] Log warning if disabled entities receive updates. (#26143) * Log warning if disabled entities receive updates. * Fix test * Always set entity ID on disabled entities --- homeassistant/helpers/entity.py | 13 ++++++++ homeassistant/helpers/entity_platform.py | 6 ++-- .../components/config/test_entity_registry.py | 21 ++++++++++--- tests/helpers/test_entity.py | 31 +++++++++++++++++-- tests/helpers/test_entity_platform.py | 2 +- 5 files changed, 63 insertions(+), 10 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index aecdf45dde5..7de41415f08 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -99,6 +99,9 @@ class Entity: # If we reported if this entity was slow _slow_reported = False + # If we reported this entity is updated while disabled + _disabled_reported = False + # Protect for multiple updates _update_staged = False @@ -273,6 +276,16 @@ class Entity: @callback def _async_write_ha_state(self): """Write the state to the state machine.""" + if self.registry_entry and self.registry_entry.disabled_by: + if not self._disabled_reported: + self._disabled_reported = True + _LOGGER.warning( + "Entity %s is incorrectly being triggered for updates while it is disabled. This is a bug in the %s integration.", + self.entity_id, + self.platform.platform_name, + ) + return + start = timer() attr = {} diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 74351ac50af..4a6a3038fd0 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -349,6 +349,9 @@ class EntityPlatform: disabled_by=disabled_by, ) + entity.registry_entry = entry + entity.entity_id = entry.entity_id + if entry.disabled: self.logger.info( "Not adding entity %s because it's disabled", @@ -358,9 +361,6 @@ class EntityPlatform: ) return - entity.registry_entry = entry - entity.entity_id = entry.entity_id - # We won't generate an entity ID if the platform has already set one # We will however make sure that platform cannot pick a registered ID elif entity.entity_id is not None and entity_registry.async_is_registered( diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index f18abe9b0e2..64328a0c8c5 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -127,13 +127,13 @@ async def test_update_entity(hass, client): assert state is not None assert state.name == "before update" + # UPDATE NAME await client.send_json( { "id": 6, "type": "config/entity_registry/update", "entity_id": "test_domain.world", "name": "after update", - "disabled_by": "user", } ) @@ -142,7 +142,7 @@ async def test_update_entity(hass, client): assert msg["result"] == { "config_entry_id": None, "device_id": None, - "disabled_by": "user", + "disabled_by": None, "platform": "test_platform", "entity_id": "test_domain.world", "name": "after update", @@ -151,13 +151,26 @@ async def test_update_entity(hass, client): state = hass.states.get("test_domain.world") assert state.name == "after update" - assert registry.entities["test_domain.world"].disabled_by == "user" - + # UPDATE DISABLED_BY TO USER await client.send_json( { "id": 7, "type": "config/entity_registry/update", "entity_id": "test_domain.world", + "disabled_by": "user", + } + ) + + msg = await client.receive_json() + + assert registry.entities["test_domain.world"].disabled_by == "user" + + # UPDATE DISABLED_BY TO NONE + await client.send_json( + { + "id": 8, + "type": "config/entity_registry/update", + "entity_id": "test_domain.world", "disabled_by": None, } ) diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 58f76d396c1..94650592d8e 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -7,13 +7,13 @@ from unittest.mock import MagicMock, patch, PropertyMock import pytest -import homeassistant.helpers.entity as entity +from homeassistant.helpers import entity, entity_registry from homeassistant.core import Context from homeassistant.const import ATTR_HIDDEN, ATTR_DEVICE_CLASS from homeassistant.config import DATA_CUSTOMIZE from homeassistant.helpers.entity_values import EntityValues -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant, mock_registry def test_generate_entity_id_requires_hass_or_ids(): @@ -499,3 +499,30 @@ async def test_set_context_expired(hass): assert hass.states.get("hello.world").context != context assert ent._context is None assert ent._context_set is None + + +async def test_warn_disabled(hass, caplog): + """Test we warn once if we write to a disabled entity.""" + entry = entity_registry.RegistryEntry( + entity_id="hello.world", + unique_id="test-unique-id", + platform="test-platform", + disabled_by="user", + ) + mock_registry(hass, {"hello.world": entry}) + + ent = entity.Entity() + ent.hass = hass + ent.entity_id = "hello.world" + ent.registry_entry = entry + ent.platform = MagicMock(platform_name="test-platform") + + caplog.clear() + ent.async_write_ha_state() + assert hass.states.get("hello.world") is None + assert "Entity hello.world is incorrectly being triggered" in caplog.text + + caplog.clear() + ent.async_write_ha_state() + assert hass.states.get("hello.world") is None + assert caplog.text == "" diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 606a4c82096..caf8bb702af 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -491,7 +491,7 @@ async def test_registry_respect_entity_disabled(hass): platform = MockEntityPlatform(hass) entity = MockEntity(unique_id="1234") await platform.async_add_entities([entity]) - assert entity.entity_id is None + assert entity.entity_id == "test_domain.world" assert hass.states.async_entity_ids() == [] From 4d656e130ddb7ed45cde7422d8934d14ff653865 Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Thu, 22 Aug 2019 22:26:08 +0300 Subject: [PATCH 253/273] Fix tuya switch state (#26145) * bump tuyaha 0.0.3 * bump tuyaha 0.0.3 --- homeassistant/components/tuya/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 57eb3f17584..8d47d8a0173 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -3,7 +3,7 @@ "name": "Tuya", "documentation": "https://www.home-assistant.io/components/tuya", "requirements": [ - "tuyaha==0.0.2" + "tuyaha==0.0.3" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index a2b91f7eb7d..d3d60e6a43e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1854,7 +1854,7 @@ tplink==0.2.1 transmissionrpc==0.11 # homeassistant.components.tuya -tuyaha==0.0.2 +tuyaha==0.0.3 # homeassistant.components.twentemilieu twentemilieu==0.1.0 From c7477f00f533922cf8262e7e1a2105d65438d716 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 22 Aug 2019 15:09:26 -0700 Subject: [PATCH 254/273] Bumped version to 0.98.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index aebcb95c3b1..a2a79eee249 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 98 -PATCH_VERSION = "0b0" +PATCH_VERSION = "0b1" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From e4906c277a111b135b5becb241cc0724a3d53bbb Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 23 Aug 2019 13:55:23 +0200 Subject: [PATCH 255/273] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 6b986329291..2e537fbb774 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -198,7 +198,7 @@ stages: } sudo docker pull homeassistant/amd64-homeassistant:$(Build.SourceBranchName) - sudo docker pull homeassistant/i368-homeassistant:$(Build.SourceBranchName) + sudo docker pull homeassistant/i386-homeassistant:$(Build.SourceBranchName) sudo docker pull homeassistant/armhf-homeassistant:$(Build.SourceBranchName) sudo docker pull homeassistant/armv7-homeassistant:$(Build.SourceBranchName) sudo docker pull homeassistant/aarch64-homeassistant:$(Build.SourceBranchName) From 05ed3c44eacba77d631e4da3af78187c9f2f70f9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 25 Aug 2019 22:24:46 -0700 Subject: [PATCH 256/273] Updated frontend to 20190825.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 8d6271183bd..78f87639a99 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==20190822.0" + "home-assistant-frontend==20190825.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0f4fb56970b..873a5aaf31d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.17 -home-assistant-frontend==20190822.0 +home-assistant-frontend==20190825.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index d3d60e6a43e..c9475f4b65b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -624,7 +624,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190822.0 +home-assistant-frontend==20190825.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b5d139719ef..50f49296247 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -176,7 +176,7 @@ hdate==0.9.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190822.0 +home-assistant-frontend==20190825.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 65cf5a6ef5cf877666e24fb365978b6577e6d81e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 22 Aug 2019 17:32:43 -0700 Subject: [PATCH 257/273] Reload config entry when entity enabled in entity registry, remove entity if disabled. (#26120) * Reload config entry when disabled_by updated in entity registry * Add types * Remove entities that get disabled * Remove unnecessary domain checks. * Attach handler in async_setup * Remove unused var * Type * Fix test * Fix tests --- homeassistant/config_entries.py | 116 ++++++++++++++++-- homeassistant/helpers/entity.py | 4 + homeassistant/helpers/entity_registry.py | 2 +- .../components/config/test_entity_registry.py | 1 + tests/helpers/test_entity.py | 31 +++++ tests/helpers/test_entity_registry.py | 1 + tests/test_config_entries.py | 76 ++++++++++++ 7 files changed, 219 insertions(+), 12 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 2e1fbea14d1..c2da37943c1 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -3,13 +3,7 @@ import asyncio import logging import functools import uuid -from typing import ( - Any, - Callable, - List, - Optional, - Set, # noqa pylint: disable=unused-import -) +from typing import Any, Callable, List, Optional, Set import weakref import attr @@ -19,6 +13,7 @@ from homeassistant.core import callback, HomeAssistant from homeassistant.exceptions import HomeAssistantError, ConfigEntryNotReady from homeassistant.setup import async_setup_component, async_process_deps_reqs from homeassistant.util.decorator import Registry +from homeassistant.helpers import entity_registry # mypy: allow-untyped-defs @@ -161,8 +156,6 @@ class ConfigEntry: 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", @@ -174,8 +167,20 @@ class ConfigEntry: self.state = ENTRY_STATE_SETUP_ERROR return - # Perform migration - if integration.domain == self.domain: + if self.domain == integration.domain: + try: + integration.get_platform("config_flow") + except ImportError as err: + _LOGGER.error( + "Error importing platform config_flow from integration %s to set up %s config entry: %s", + integration.domain, + self.domain, + err, + ) + self.state = ENTRY_STATE_SETUP_ERROR + return + + # Perform migration if not await self.async_migrate(hass): self.state = ENTRY_STATE_MIGRATION_ERROR return @@ -383,6 +388,7 @@ class ConfigEntries: self._hass_config = hass_config self._entries = [] # type: List[ConfigEntry] self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + EntityRegistryDisabledHandler(hass).async_setup() @callback def async_domains(self) -> List[str]: @@ -757,3 +763,91 @@ class SystemOptions: def as_dict(self): """Return dictionary version of this config entrys system options.""" return {"disable_new_entities": self.disable_new_entities} + + +class EntityRegistryDisabledHandler: + """Handler to handle when entities related to config entries updating disabled_by.""" + + RELOAD_AFTER_UPDATE_DELAY = 30 + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the handler.""" + self.hass = hass + self.registry: Optional[entity_registry.EntityRegistry] = None + self.changed: Set[str] = set() + self._remove_call_later: Optional[Callable[[], None]] = None + + @callback + def async_setup(self) -> None: + """Set up the disable handler.""" + self.hass.bus.async_listen( + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, self._handle_entry_updated + ) + + async def _handle_entry_updated(self, event): + """Handle entity registry entry update.""" + if ( + event.data["action"] != "update" + or "disabled_by" not in event.data["changes"] + ): + return + + if self.registry is None: + self.registry = await entity_registry.async_get_registry(self.hass) + + entity_entry = self.registry.async_get(event.data["entity_id"]) + + if ( + # Stop if no entry found + entity_entry is None + # Stop if entry not connected to config entry + or entity_entry.config_entry_id is None + # Stop if the entry got disabled. In that case the entity handles it + # themselves. + or entity_entry.disabled_by + ): + return + + config_entry = self.hass.config_entries.async_get_entry( + entity_entry.config_entry_id + ) + + if config_entry.entry_id not in self.changed and await support_entry_unload( + self.hass, config_entry.domain + ): + self.changed.add(config_entry.entry_id) + + if not self.changed: + return + + # We are going to delay reloading on *every* entity registry change so that + # if a user is happily clicking along, it will only reload at the end. + + if self._remove_call_later: + self._remove_call_later() + + self._remove_call_later = self.hass.helpers.event.async_call_later( + self.RELOAD_AFTER_UPDATE_DELAY, self._handle_reload + ) + + async def _handle_reload(self, _now): + """Handle a reload.""" + self._remove_call_later = None + to_reload = self.changed + self.changed = set() + + _LOGGER.info( + "Reloading config entries because disabled_by changed in entity registry: %s", + ", ".join(self.changed), + ) + + await asyncio.gather( + *[self.hass.config_entries.async_reload(entry_id) for entry_id in to_reload] + ) + + +async def support_entry_unload(hass: HomeAssistant, domain: str) -> bool: + """Test if a domain supports entry unloading.""" + integration = await loader.async_get_integration(hass, domain) + component = integration.get_component() + return hasattr(component, "async_unload_entry") diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 7de41415f08..bd96e1bafdb 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -503,6 +503,10 @@ class Entity: old = self.registry_entry self.registry_entry = ent_reg.async_get(data["entity_id"]) + if self.registry_entry.disabled_by is not None: + await self.async_remove() + return + if self.registry_entry.entity_id == old.entity_id: self.async_write_ha_state() return diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 3d84313a5c6..7d81f62fa1c 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -302,7 +302,7 @@ class EntityRegistry: self.async_schedule_save() - data = {"action": "update", "entity_id": entity_id} + data = {"action": "update", "entity_id": entity_id, "changes": list(changes)} if old.entity_id != entity_id: data["old_entity_id"] = old.entity_id diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 64328a0c8c5..9472d888254 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -163,6 +163,7 @@ async def test_update_entity(hass, client): msg = await client.receive_json() + assert hass.states.get("test_domain.world") is None assert registry.entities["test_domain.world"].disabled_by == "user" # UPDATE DISABLED_BY TO NONE diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 94650592d8e..3c89a5c6537 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -526,3 +526,34 @@ async def test_warn_disabled(hass, caplog): ent.async_write_ha_state() assert hass.states.get("hello.world") is None assert caplog.text == "" + + +async def test_disabled_in_entity_registry(hass): + """Test entity is removed if we disable entity registry entry.""" + entry = entity_registry.RegistryEntry( + entity_id="hello.world", + unique_id="test-unique-id", + platform="test-platform", + disabled_by="user", + ) + registry = mock_registry(hass, {"hello.world": entry}) + + ent = entity.Entity() + ent.hass = hass + ent.entity_id = "hello.world" + ent.registry_entry = entry + ent.platform = MagicMock(platform_name="test-platform") + + await ent.async_internal_added_to_hass() + ent.async_write_ha_state() + assert hass.states.get("hello.world") is None + + entry2 = registry.async_update_entity("hello.world", disabled_by=None) + await hass.async_block_till_done() + assert entry2 != entry + assert ent.registry_entry == entry2 + + entry3 = registry.async_update_entity("hello.world", disabled_by="user") + await hass.async_block_till_done() + assert entry3 != entry2 + assert ent.registry_entry == entry3 diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index aee6b6f19a3..9debbdbcba7 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -219,6 +219,7 @@ async def test_updating_config_entry_id(hass, registry, update_events): assert update_events[0]["entity_id"] == entry.entity_id assert update_events[1]["action"] == "update" assert update_events[1]["entity_id"] == entry.entity_id + assert update_events[1]["changes"] == ["config_entry_id"] async def test_removing_config_entry_id(hass, registry, update_events): diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index ca6872a7a2c..d9dd614c9a5 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -20,6 +20,7 @@ from tests.common import ( MockEntity, mock_integration, mock_entity_platform, + mock_registry, ) @@ -925,3 +926,78 @@ async def test_init_custom_integration(hass): return_value=mock_coro(integration), ): await hass.config_entries.flow.async_init("bla") + + +async def test_support_entry_unload(hass): + """Test unloading entry.""" + assert await config_entries.support_entry_unload(hass, "light") + assert not await config_entries.support_entry_unload(hass, "auth") + + +async def test_reload_entry_entity_registry_ignores_no_entry(hass): + """Test reloading entry in entity registry skips if no config entry linked.""" + handler = config_entries.EntityRegistryDisabledHandler(hass) + registry = mock_registry(hass) + + # Test we ignore entities without config entry + entry = registry.async_get_or_create("light", "hue", "123") + registry.async_update_entity(entry.entity_id, disabled_by="user") + await hass.async_block_till_done() + assert not handler.changed + assert handler._remove_call_later is None + + +async def test_reload_entry_entity_registry_works(hass): + """Test we schedule an entry to be reloaded if disabled_by is updated.""" + handler = config_entries.EntityRegistryDisabledHandler(hass) + handler.async_setup() + registry = mock_registry(hass) + + config_entry = MockConfigEntry( + domain="comp", state=config_entries.ENTRY_STATE_LOADED + ) + config_entry.add_to_hass(hass) + mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_unload_entry = MagicMock(return_value=mock_coro(True)) + mock_integration( + hass, + MockModule( + "comp", + async_setup_entry=mock_setup_entry, + async_unload_entry=mock_unload_entry, + ), + ) + mock_entity_platform(hass, "config_flow.comp", None) + + # Only changing disabled_by should update trigger + entity_entry = registry.async_get_or_create( + "light", "hue", "123", config_entry=config_entry + ) + registry.async_update_entity(entity_entry.entity_id, name="yo") + await hass.async_block_till_done() + assert not handler.changed + assert handler._remove_call_later is None + + # Disable entity, we should not do anything, only act when enabled. + registry.async_update_entity(entity_entry.entity_id, disabled_by="user") + await hass.async_block_till_done() + assert not handler.changed + assert handler._remove_call_later is None + + # Enable entity, check we are reloading config entry. + registry.async_update_entity(entity_entry.entity_id, disabled_by=None) + await hass.async_block_till_done() + assert handler.changed == {config_entry.entry_id} + assert handler._remove_call_later is not None + + async_fire_time_changed( + hass, + dt.utcnow() + + timedelta( + seconds=config_entries.EntityRegistryDisabledHandler.RELOAD_AFTER_UPDATE_DELAY + + 1 + ), + ) + await hass.async_block_till_done() + + assert len(mock_unload_entry.mock_calls) == 1 From 45a454ba535a884191987732aad2a52c678a0281 Mon Sep 17 00:00:00 2001 From: On Freund Date: Fri, 23 Aug 2019 16:59:25 +0300 Subject: [PATCH 258/273] CoolMaster: Change auto to heat_cool (#26144) --- homeassistant/components/coolmaster/climate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 7379d66777b..8a319c655f6 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA from homeassistant.components.climate.const import ( HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, @@ -33,14 +33,14 @@ AVAILABLE_MODES = [ HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_DRY, - HVAC_MODE_AUTO, + HVAC_MODE_HEAT_COOL, HVAC_MODE_FAN_ONLY, ] CM_TO_HA_STATE = { "heat": HVAC_MODE_HEAT, "cool": HVAC_MODE_COOL, - "auto": HVAC_MODE_AUTO, + "auto": HVAC_MODE_HEAT_COOL, "dry": HVAC_MODE_DRY, "fan": HVAC_MODE_FAN_ONLY, } From ee03f5d7c1445fafeac643efe5fc028b0619b3e1 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Fri, 23 Aug 2019 06:58:24 -0700 Subject: [PATCH 259/273] Bump androidtv to 0.0.24 (#26158) * Bump androidtv to 0.0.24 * Add unique ID for Fire TV (not just Android TV) --- homeassistant/components/androidtv/manifest.json | 2 +- .../components/androidtv/media_player.py | 15 ++++++++------- requirements_all.txt | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 24eb61d52b0..047eaaaf5db 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,7 +3,7 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/components/androidtv", "requirements": [ - "androidtv==0.0.23" + "androidtv==0.0.24" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index ef9293381fd..db4ff9e851e 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -270,6 +270,9 @@ class ADBDevice(MediaPlayerDevice): self._apps.update(apps) self._keys = KEYS + self._device_properties = self.aftv.device_properties + self._unique_id = self._device_properties.get("serialno") + self.turn_on_command = turn_on_command self.turn_off_command = turn_off_command @@ -338,6 +341,11 @@ class ADBDevice(MediaPlayerDevice): """Return the state of the player.""" return self._state + @property + def unique_id(self): + """Return the device unique id.""" + return self._unique_id + @adb_decorator() def media_play(self): """Send play command.""" @@ -412,9 +420,7 @@ class AndroidTVDevice(ADBDevice): super().__init__(aftv, name, apps, turn_on_command, turn_off_command) self._device = None - self._device_properties = self.aftv.device_properties self._is_volume_muted = None - self._unique_id = self._device_properties.get("serialno") self._volume_level = None @adb_decorator(override_available=True) @@ -454,11 +460,6 @@ class AndroidTVDevice(ADBDevice): """Flag media player features that are supported.""" return SUPPORT_ANDROIDTV - @property - def unique_id(self): - """Return the device unique id.""" - return self._unique_id - @property def volume_level(self): """Return the volume level.""" diff --git a/requirements_all.txt b/requirements_all.txt index c9475f4b65b..70483d1f2e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -194,7 +194,7 @@ ambiclimate==0.2.0 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.23 +androidtv==0.0.24 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 From b64ac5be8564d8d11778e1b64fcb46293985bd2c Mon Sep 17 00:00:00 2001 From: Chao Date: Fri, 23 Aug 2019 13:14:18 -0400 Subject: [PATCH 260/273] fix issue setting scan_interval (#26165) I was getting the following error when i set the scan_interval ``` self.scan_interval = timedelta(seconds=config.get(CONF_SCAN_INTERVAL, 60)) TypeError: unsupported type for timedelta seconds component: datetime.timedelta ``` it turns out `config.get(CONF_SCAN_INTERVAL)` already returns `timedelta` ```('scan_interval', datetime.timedelta(seconds=180))``` --- homeassistant/components/google_maps/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index 0887aa19bfb..2149e40e504 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -52,7 +52,7 @@ class GoogleMapsScanner: self.see = see self.username = config[CONF_USERNAME] self.max_gps_accuracy = config[CONF_MAX_GPS_ACCURACY] - self.scan_interval = timedelta(seconds=config.get(CONF_SCAN_INTERVAL, 60)) + self.scan_interval = config.get(CONF_SCAN_INTERVAL) or timedelta(60) credfile = "{}.{}".format( hass.config.path(CREDENTIALS_FILE), slugify(self.username) From afab0a956822e6035222ac3843293aa3041a4ae2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 24 Aug 2019 18:18:31 -0600 Subject: [PATCH 261/273] Fix possible KeyError in SimpliSafe (#26190) --- homeassistant/components/simplisafe/alarm_control_panel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 028121a9663..d44a1c7760a 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -170,7 +170,7 @@ class SimpliSafeAlarm(AlarmControlPanel): """Update alarm status.""" event_data = self._simplisafe.last_event_data[self._system.system_id] - if event_data["pinName"]: + if event_data.get("pinName"): self._changed_by = event_data["pinName"] if self._system.state == SystemStates.error: From 677995a05a604efb44730164e1c1e57d4a7d79c4 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Sun, 25 Aug 2019 13:57:43 -0500 Subject: [PATCH 262/273] Update pyheos to 0.6.0 (#26191) --- homeassistant/components/heos/__init__.py | 26 ++++-------- homeassistant/components/heos/config_flow.py | 6 +-- homeassistant/components/heos/manifest.json | 4 +- homeassistant/components/heos/media_player.py | 10 +---- homeassistant/components/heos/services.py | 9 ++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/heos/test_config_flow.py | 23 +++++------ tests/components/heos/test_init.py | 40 +++++++++---------- tests/components/heos/test_media_player.py | 28 ++++++------- tests/components/heos/test_services.py | 8 ++-- 11 files changed, 66 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index a5450253be0..20ed7930a4f 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -4,7 +4,7 @@ from datetime import timedelta import logging from typing import Dict -from pyheos import CommandError, Heos, const as heos_const +from pyheos import Heos, HeosError, const as heos_const import voluptuous as vol from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN @@ -68,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): try: await controller.connect(auto_reconnect=True) # Auto reconnect only operates if initial connection was successful. - except (asyncio.TimeoutError, ConnectionError, CommandError) as error: + except HeosError as error: await controller.disconnect() _LOGGER.debug("Unable to connect to controller %s: %s", host, error) raise ConfigEntryNotReady @@ -93,13 +93,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): host, ) inputs = await controller.get_input_sources() - except (asyncio.TimeoutError, ConnectionError, CommandError) as error: + except HeosError as error: await controller.disconnect() - _LOGGER.debug( - "Unable to retrieve players and sources: %s", - error, - exc_info=isinstance(error, CommandError), - ) + _LOGGER.debug("Unable to retrieve players and sources: %s", error) raise ConfigEntryNotReady controller_manager = ControllerManager(hass, controller) @@ -187,7 +183,7 @@ class ControllerManager: # Retrieve latest players and refresh status data = await self.controller.load_players() self.update_ids(data[heos_const.DATA_MAPPED_IDS]) - except (CommandError, asyncio.TimeoutError, ConnectionError) as ex: + except HeosError as ex: _LOGGER.error("Unable to refresh players: %s", ex) # Update players self._hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_HEOS_UPDATED) @@ -312,21 +308,15 @@ class SourceManager: favorites = await controller.get_favorites() inputs = await controller.get_input_sources() return favorites, inputs - except (asyncio.TimeoutError, ConnectionError, CommandError) as error: + except HeosError as error: if retry_attempts < self.max_retry_attempts: retry_attempts += 1 _LOGGER.debug( - "Error retrieving sources and will " "retry: %s", - error, - exc_info=isinstance(error, CommandError), + "Error retrieving sources and will " "retry: %s", error ) await asyncio.sleep(self.retry_delay) else: - _LOGGER.error( - "Unable to update sources: %s", - error, - exc_info=isinstance(error, CommandError), - ) + _LOGGER.error("Unable to update sources: %s", error) return async def update_sources(event, data=None): diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py index 7c7f57a91d7..1d56478ba3a 100644 --- a/homeassistant/components/heos/config_flow.py +++ b/homeassistant/components/heos/config_flow.py @@ -1,7 +1,5 @@ """Config flow to configure Heos.""" -import asyncio - -from pyheos import Heos +from pyheos import Heos, HeosError import voluptuous as vol from homeassistant import config_entries @@ -59,7 +57,7 @@ class HeosFlowHandler(config_entries.ConfigFlow): await heos.connect() self.hass.data.pop(DATA_DISCOVERED_HOSTS) return await self.async_step_import({CONF_HOST: host}) - except (asyncio.TimeoutError, ConnectionError): + except HeosError: errors[CONF_HOST] = "connection_failure" finally: await heos.disconnect() diff --git a/homeassistant/components/heos/manifest.json b/homeassistant/components/heos/manifest.json index 09833bb729b..eb9ef258a3c 100644 --- a/homeassistant/components/heos/manifest.json +++ b/homeassistant/components/heos/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/heos", "requirements": [ - "pyheos==0.5.2" + "pyheos==0.6.0" ], "ssdp": { "st": [ @@ -15,4 +15,4 @@ "codeowners": [ "@andrewsayre" ] -} +} \ No newline at end of file diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index a4094a0c216..40f6113a80d 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -1,11 +1,10 @@ """Denon HEOS Media Player.""" -import asyncio from functools import reduce, wraps import logging from operator import ior from typing import Sequence -from pyheos import CommandError, const as heos_const +from pyheos import HeosError, const as heos_const from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( @@ -83,12 +82,7 @@ def log_command_error(command: str): async def wrapper(*args, **kwargs): try: await func(*args, **kwargs) - except ( - CommandError, - asyncio.TimeoutError, - ConnectionError, - ValueError, - ) as ex: + except (HeosError, ValueError) as ex: _LOGGER.error("Unable to %s: %s", command, ex) return wrapper diff --git a/homeassistant/components/heos/services.py b/homeassistant/components/heos/services.py index 8f3521399e2..ee5df1b483b 100644 --- a/homeassistant/components/heos/services.py +++ b/homeassistant/components/heos/services.py @@ -1,9 +1,8 @@ """Services for the HEOS integration.""" -import asyncio import functools import logging -from pyheos import CommandError, Heos, const +from pyheos import CommandFailedError, Heos, HeosError, const import voluptuous as vol from homeassistant.helpers import config_validation as cv @@ -57,9 +56,9 @@ async def _sign_in_handler(controller, service): password = service.data[ATTR_PASSWORD] try: await controller.sign_in(username, password) - except CommandError as err: + except CommandFailedError as err: _LOGGER.error("Sign in failed: %s", err) - except (asyncio.TimeoutError, ConnectionError) as err: + except HeosError as err: _LOGGER.error("Unable to sign in: %s", err) @@ -70,5 +69,5 @@ async def _sign_out_handler(controller, service): return try: await controller.sign_out() - except (asyncio.TimeoutError, ConnectionError, CommandError) as err: + except HeosError as err: _LOGGER.error("Unable to sign out: %s", err) diff --git a/requirements_all.txt b/requirements_all.txt index 70483d1f2e2..b467859343f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1186,7 +1186,7 @@ pygtt==1.1.2 pyhaversion==3.0.2 # homeassistant.components.heos -pyheos==0.5.2 +pyheos==0.6.0 # homeassistant.components.hikvision pyhik==0.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 50f49296247..96b82caf968 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -277,7 +277,7 @@ pydeconz==62 pydispatcher==2.0.5 # homeassistant.components.heos -pyheos==0.5.2 +pyheos==0.6.0 # homeassistant.components.homematic pyhomematic==0.1.60 diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index 0d834ccc770..df021fea55d 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -1,5 +1,5 @@ """Tests for the Heos config flow module.""" -import asyncio +from pyheos import HeosError from homeassistant import data_entry_flow from homeassistant.components.heos.config_flow import HeosFlowHandler @@ -31,18 +31,15 @@ async def test_cannot_connect_shows_error_form(hass, controller): """Test form is shown with error when cannot connect.""" flow = HeosFlowHandler() flow.hass = hass - - errors = [ConnectionError, asyncio.TimeoutError] - for error in errors: - controller.connect.side_effect = error - result = await flow.async_step_user({CONF_HOST: "127.0.0.1"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - assert result["errors"][CONF_HOST] == "connection_failure" - assert controller.connect.call_count == 1 - assert controller.disconnect.call_count == 1 - controller.connect.reset_mock() - controller.disconnect.reset_mock() + controller.connect.side_effect = HeosError() + result = await flow.async_step_user({CONF_HOST: "127.0.0.1"}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"][CONF_HOST] == "connection_failure" + assert controller.connect.call_count == 1 + assert controller.disconnect.call_count == 1 + controller.connect.reset_mock() + controller.disconnect.reset_mock() async def test_create_entry_when_host_valid(hass, controller): diff --git a/tests/components/heos/test_init.py b/tests/components/heos/test_init.py index 728e65b81f5..7b2645cb8ec 100644 --- a/tests/components/heos/test_init.py +++ b/tests/components/heos/test_init.py @@ -2,7 +2,7 @@ import asyncio from asynctest import Mock, patch -from pyheos import CommandError, const +from pyheos import CommandFailedError, HeosError, const import pytest from homeassistant.components.heos import ( @@ -117,31 +117,27 @@ async def test_async_setup_entry_not_signed_in_loads_platforms( async def test_async_setup_entry_connect_failure(hass, config_entry, controller): """Connection failure raises ConfigEntryNotReady.""" config_entry.add_to_hass(hass) - errors = [ConnectionError, asyncio.TimeoutError] - for error in errors: - controller.connect.side_effect = error - with pytest.raises(ConfigEntryNotReady): - await async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - assert controller.connect.call_count == 1 - assert controller.disconnect.call_count == 1 - controller.connect.reset_mock() - controller.disconnect.reset_mock() + controller.connect.side_effect = HeosError() + with pytest.raises(ConfigEntryNotReady): + await async_setup_entry(hass, config_entry) + await hass.async_block_till_done() + assert controller.connect.call_count == 1 + assert controller.disconnect.call_count == 1 + controller.connect.reset_mock() + controller.disconnect.reset_mock() async def test_async_setup_entry_player_failure(hass, config_entry, controller): """Failure to retrieve players/sources raises ConfigEntryNotReady.""" config_entry.add_to_hass(hass) - errors = [ConnectionError, asyncio.TimeoutError] - for error in errors: - controller.get_players.side_effect = error - with pytest.raises(ConfigEntryNotReady): - await async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - assert controller.connect.call_count == 1 - assert controller.disconnect.call_count == 1 - controller.connect.reset_mock() - controller.disconnect.reset_mock() + controller.get_players.side_effect = HeosError() + with pytest.raises(ConfigEntryNotReady): + await async_setup_entry(hass, config_entry) + await hass.async_block_till_done() + assert controller.connect.call_count == 1 + assert controller.disconnect.call_count == 1 + controller.connect.reset_mock() + controller.disconnect.reset_mock() async def test_unload_entry(hass, config_entry, controller): @@ -167,7 +163,7 @@ async def test_update_sources_retry(hass, config_entry, config, controller, capl source_manager = hass.data[DOMAIN][DATA_SOURCE_MANAGER] source_manager.retry_delay = 0 source_manager.max_retry_attempts = 1 - controller.get_favorites.side_effect = CommandError("Test", "test", 0) + controller.get_favorites.side_effect = CommandFailedError("Test", "test", 0) controller.dispatcher.send( const.SIGNAL_CONTROLLER_EVENT, const.EVENT_SOURCES_CHANGED, {} ) diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index de062757803..0f9bf2d8b3e 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -1,7 +1,7 @@ """Tests for the Heos Media Player platform.""" import asyncio -from pyheos import CommandError, const +from pyheos import CommandFailedError, const from homeassistant.components.heos import media_player from homeassistant.components.heos.const import ( @@ -179,7 +179,7 @@ async def test_updates_from_connection_event( event.clear() player.reset_mock() controller.load_players.reset_mock() - controller.load_players.side_effect = CommandError(None, "Failure", 1) + controller.load_players.side_effect = CommandFailedError(None, "Failure", 1) player.available = True player.heos.dispatcher.send(const.SIGNAL_HEOS_EVENT, const.EVENT_CONNECTED) await event.wait() @@ -313,7 +313,7 @@ async def test_clear_playlist(hass, config_entry, config, controller, caplog): ) assert player.clear_queue.call_count == 1 player.clear_queue.reset_mock() - player.clear_queue.side_effect = CommandError(None, "Failure", 1) + player.clear_queue.side_effect = CommandFailedError(None, "Failure", 1) assert "Unable to clear playlist: Failure (1)" in caplog.text @@ -331,7 +331,7 @@ async def test_pause(hass, config_entry, config, controller, caplog): ) assert player.pause.call_count == 1 player.pause.reset_mock() - player.pause.side_effect = CommandError(None, "Failure", 1) + player.pause.side_effect = CommandFailedError(None, "Failure", 1) assert "Unable to pause: Failure (1)" in caplog.text @@ -349,7 +349,7 @@ async def test_play(hass, config_entry, config, controller, caplog): ) assert player.play.call_count == 1 player.play.reset_mock() - player.play.side_effect = CommandError(None, "Failure", 1) + player.play.side_effect = CommandFailedError(None, "Failure", 1) assert "Unable to play: Failure (1)" in caplog.text @@ -367,7 +367,7 @@ async def test_previous_track(hass, config_entry, config, controller, caplog): ) assert player.play_previous.call_count == 1 player.play_previous.reset_mock() - player.play_previous.side_effect = CommandError(None, "Failure", 1) + player.play_previous.side_effect = CommandFailedError(None, "Failure", 1) assert "Unable to move to previous track: Failure (1)" in caplog.text @@ -385,7 +385,7 @@ async def test_next_track(hass, config_entry, config, controller, caplog): ) assert player.play_next.call_count == 1 player.play_next.reset_mock() - player.play_next.side_effect = CommandError(None, "Failure", 1) + player.play_next.side_effect = CommandFailedError(None, "Failure", 1) assert "Unable to move to next track: Failure (1)" in caplog.text @@ -403,7 +403,7 @@ async def test_stop(hass, config_entry, config, controller, caplog): ) assert player.stop.call_count == 1 player.stop.reset_mock() - player.stop.side_effect = CommandError(None, "Failure", 1) + player.stop.side_effect = CommandFailedError(None, "Failure", 1) assert "Unable to stop: Failure (1)" in caplog.text @@ -421,7 +421,7 @@ async def test_volume_mute(hass, config_entry, config, controller, caplog): ) assert player.set_mute.call_count == 1 player.set_mute.reset_mock() - player.set_mute.side_effect = CommandError(None, "Failure", 1) + player.set_mute.side_effect = CommandFailedError(None, "Failure", 1) assert "Unable to set mute: Failure (1)" in caplog.text @@ -439,7 +439,7 @@ async def test_shuffle_set(hass, config_entry, config, controller, caplog): ) player.set_play_mode.assert_called_once_with(player.repeat, True) player.set_play_mode.reset_mock() - player.set_play_mode.side_effect = CommandError(None, "Failure", 1) + player.set_play_mode.side_effect = CommandFailedError(None, "Failure", 1) assert "Unable to set shuffle: Failure (1)" in caplog.text @@ -457,7 +457,7 @@ async def test_volume_set(hass, config_entry, config, controller, caplog): ) player.set_volume.assert_called_once_with(100) player.set_volume.reset_mock() - player.set_volume.side_effect = CommandError(None, "Failure", 1) + player.set_volume.side_effect = CommandFailedError(None, "Failure", 1) assert "Unable to set volume level: Failure (1)" in caplog.text @@ -516,7 +516,7 @@ async def test_select_radio_favorite_command_error( player = controller.players[1] # Test set radio preset favorite = favorites[2] - player.play_favorite.side_effect = CommandError(None, "Failure", 1) + player.play_favorite.side_effect = CommandFailedError(None, "Failure", 1) await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_SELECT_SOURCE, @@ -575,7 +575,7 @@ async def test_select_input_command_error( await setup_platform(hass, config_entry, config) player = controller.players[1] input_source = input_sources[0] - player.play_input_source.side_effect = CommandError(None, "Failure", 1) + player.play_input_source.side_effect = CommandFailedError(None, "Failure", 1) await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_SELECT_SOURCE, @@ -615,7 +615,7 @@ async def test_play_media_url(hass, config_entry, config, controller, caplog): ) player.play_url.assert_called_once_with(url) player.play_url.reset_mock() - player.play_url.side_effect = CommandError(None, "Failure", 1) + player.play_url.side_effect = CommandFailedError(None, "Failure", 1) assert "Unable to play media: Failure (1)" in caplog.text diff --git a/tests/components/heos/test_services.py b/tests/components/heos/test_services.py index 0e1cbc8ea2e..5a835cf7303 100644 --- a/tests/components/heos/test_services.py +++ b/tests/components/heos/test_services.py @@ -1,5 +1,5 @@ """Tests for the services module.""" -from pyheos import CommandError, const +from pyheos import CommandFailedError, HeosError, const from homeassistant.components.heos.const import ( ATTR_PASSWORD, @@ -51,7 +51,7 @@ async def test_sign_in_not_connected(hass, config_entry, controller, caplog): async def test_sign_in_failed(hass, config_entry, controller, caplog): """Test sign-in service logs error when not connected.""" await setup_component(hass, config_entry) - controller.sign_in.side_effect = CommandError("", "Invalid credentials", 6) + controller.sign_in.side_effect = CommandFailedError("", "Invalid credentials", 6) await hass.services.async_call( DOMAIN, @@ -67,7 +67,7 @@ async def test_sign_in_failed(hass, config_entry, controller, caplog): async def test_sign_in_unknown_error(hass, config_entry, controller, caplog): """Test sign-in service logs error for failure.""" await setup_component(hass, config_entry) - controller.sign_in.side_effect = ConnectionError + controller.sign_in.side_effect = HeosError() await hass.services.async_call( DOMAIN, @@ -103,7 +103,7 @@ async def test_sign_out_not_connected(hass, config_entry, controller, caplog): async def test_sign_out_unknown_error(hass, config_entry, controller, caplog): """Test the sign-out service.""" await setup_component(hass, config_entry) - controller.sign_out.side_effect = ConnectionError + controller.sign_out.side_effect = HeosError() await hass.services.async_call(DOMAIN, SERVICE_SIGN_OUT, {}, blocking=True) From 9d51262559865876f9e6670ec284f35794680436 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 26 Aug 2019 01:34:43 -0400 Subject: [PATCH 263/273] bump quirks version (#26198) --- 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 0e004893033..8e7de41e626 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.9.1", - "zha-quirks==0.0.21", + "zha-quirks==0.0.22", "zigpy-deconz==0.2.2", "zigpy-homeassistant==0.7.1", "zigpy-xbee-homeassistant==0.4.0", diff --git a/requirements_all.txt b/requirements_all.txt index b467859343f..42982b34134 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1971,7 +1971,7 @@ zengge==0.2 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.21 +zha-quirks==0.0.22 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 From 9ad1a1ca1512da22b74c1fca87391001f42c5423 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 25 Aug 2019 22:37:34 -0700 Subject: [PATCH 264/273] Bumped version to 0.98.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a2a79eee249..4d2998b85b8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 98 -PATCH_VERSION = "0b1" +PATCH_VERSION = "0b2" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From 60256cca174fce680143f9a0feb6df790e054e81 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 26 Aug 2019 11:46:46 +0200 Subject: [PATCH 265/273] Nightly builds (#26204) * Nightly docker builds / Hass.io dev HA * use same style * Finish nightly build * Update builder version * Fix style * fix style part 2 * Last one * Fix order --- azure-pipelines-release.yml | 58 +++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 2e537fbb774..fab10bfeee6 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -3,11 +3,18 @@ trigger: tags: include: - - '*' + - '*' pr: none +schedules: + - cron: "0 1 * * *" + displayName: "nightly builds" + branches: + include: + - dev + always: true variables: - name: versionBuilder - value: '6.1' + value: '6.3' - group: docker - group: github - group: twine @@ -18,12 +25,13 @@ resources: name: 'home-assistant/ci-azure' endpoint: 'home-assistant' - stages: - stage: 'Validate' jobs: - template: templates/azp-job-version.yaml@azure + parameters: + ignoreDev: true - job: 'Permission' pool: vmImage: 'ubuntu-latest' @@ -42,10 +50,12 @@ stages: echo "${created_by} is not allowed to create an release!" exit 1 displayName: 'Check rights' + condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags')) - stage: 'Build' jobs: - job: 'ReleasePython' + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') pool: vmImage: 'ubuntu-latest' steps: @@ -86,6 +96,7 @@ stages: buildArch: 'aarch64' buildMachine: 'qemuarm-64,raspberrypi3-64,raspberrypi4-64,odroid-c2,orangepi-prime' steps: + - template: templates/azp-step-ha-version.yaml@azure - script: sudo docker login -u $(dockerUser) -p $(dockerPassword) displayName: 'Docker hub login' - script: sudo docker pull homeassistant/amd64-builder:$(versionBuilder) @@ -94,10 +105,11 @@ stages: set -e sudo docker run --rm --privileged \ - -v ~/.docker:/root/.docker \ + -v ~/.docker:/root/.docker:rw \ -v /run/docker.sock:/run/docker.sock:rw \ + -v $(pwd):/homeassistant:ro \ homeassistant/amd64-builder:$(versionBuilder) \ - --homeassistant $(Build.SourceBranchName) "--$(buildArch)" \ + --homeassistant $(homeassistantRelease) "--$(buildArch)" \ -r https://github.com/home-assistant/hassio-homeassistant \ -t generic --docker-hub homeassistant @@ -105,7 +117,7 @@ stages: -v ~/.docker:/root/.docker \ -v /run/docker.sock:/run/docker.sock:rw \ homeassistant/amd64-builder:$(versionBuilder) \ - --homeassistant-machine "$(Build.SourceBranchName)=$(buildMachine)" \ + --homeassistant-machine "$(homeassistantRelease)=$(buildMachine)" \ -r https://github.com/home-assistant/hassio-homeassistant \ -t machine --docker-hub homeassistant displayName: 'Build Release' @@ -116,6 +128,7 @@ stages: pool: vmImage: 'ubuntu-latest' steps: + - template: templates/azp-step-ha-version.yaml@azure - script: | sudo apt-get install -y --no-install-recommends \ git jq curl @@ -129,7 +142,7 @@ stages: - script: | set -e - version="$(Build.SourceBranchName)" + version="$(homeassistantRelease)" git clone https://github.com/home-assistant/hassio-version cd hassio-version @@ -138,11 +151,11 @@ stages: beta_version="$(jq --raw-output '.homeassistant.default' beta.json)" stable_version="$(jq --raw-output '.homeassistant.default' stable.json)" - if [[ "$version" =~ b ]]; then + if [[ "$version" =~ d ]]; then sed -i "s|$dev_version|$version|g" dev.json + elif [[ "$version" =~ b ]]; then 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 @@ -154,6 +167,7 @@ stages: pool: vmImage: 'ubuntu-latest' steps: + - template: templates/azp-step-ha-version.yaml@azure - script: | mkdir -p ~/.docker echo '{ "experimental": "enabled" }' > .docker/config.json @@ -197,26 +211,26 @@ stages: sudo docker --config .docker manifest push --purge homeassistant/home-assistant:${tag_l} } - sudo docker pull homeassistant/amd64-homeassistant:$(Build.SourceBranchName) - sudo docker pull homeassistant/i386-homeassistant:$(Build.SourceBranchName) - sudo docker pull homeassistant/armhf-homeassistant:$(Build.SourceBranchName) - sudo docker pull homeassistant/armv7-homeassistant:$(Build.SourceBranchName) - sudo docker pull homeassistant/aarch64-homeassistant:$(Build.SourceBranchName) + sudo docker pull homeassistant/amd64-homeassistant:$(homeassistantRelease) + sudo docker pull homeassistant/i386-homeassistant:$(homeassistantRelease) + sudo docker pull homeassistant/armhf-homeassistant:$(homeassistantRelease) + sudo docker pull homeassistant/armv7-homeassistant:$(homeassistantRelease) + sudo docker pull homeassistant/aarch64-homeassistant:$(homeassistantRelease) # Create version tag - create_manifest "$(Build.SourceBranchName)" "$(Build.SourceBranchName)" + create_manifest "$(homeassistantRelease)" "$(homeassistantRelease)" # Create general tags if [[ "$version" =~ d ]]; then - create_manifest "dev" "$(Build.SourceBranchName)" + create_manifest "dev" "$(homeassistantRelease)" elif [[ "$version" =~ b ]]; then - create_manifest "beta" "$(Build.SourceBranchName)" - create_manifest "rc" "$(Build.SourceBranchName)" + create_manifest "beta" "$(homeassistantRelease)" + create_manifest "rc" "$(homeassistantRelease)" else - create_manifest "stable" "$(Build.SourceBranchName)" - create_manifest "latest" "$(Build.SourceBranchName)" - create_manifest "beta" "$(Build.SourceBranchName)" - create_manifest "rc" "$(Build.SourceBranchName)" + create_manifest "stable" "$(homeassistantRelease)" + create_manifest "latest" "$(homeassistantRelease)" + create_manifest "beta" "$(homeassistantRelease)" + create_manifest "rc" "$(homeassistantRelease)" fi displayName: 'Create Meta-Image' From efacfa3696a7567dc12ff0fb56512ed10316deb1 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 26 Aug 2019 21:03:37 +0200 Subject: [PATCH 266/273] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 41 ++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index fab10bfeee6..896aaa710c1 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -97,14 +97,15 @@ stages: buildMachine: 'qemuarm-64,raspberrypi3-64,raspberrypi4-64,odroid-c2,orangepi-prime' steps: - template: templates/azp-step-ha-version.yaml@azure - - script: sudo docker login -u $(dockerUser) -p $(dockerPassword) + - script: | + docker login -u $(dockerUser) -p $(dockerPassword) displayName: 'Docker hub login' - - script: sudo docker pull homeassistant/amd64-builder:$(versionBuilder) + - script: docker pull homeassistant/amd64-builder:$(versionBuilder) displayName: 'Install Builder' - script: | set -e - sudo docker run --rm --privileged \ + docker run --rm --privileged \ -v ~/.docker:/root/.docker:rw \ -v /run/docker.sock:/run/docker.sock:rw \ -v $(pwd):/homeassistant:ro \ @@ -113,7 +114,7 @@ stages: -r https://github.com/home-assistant/hassio-homeassistant \ -t generic --docker-hub homeassistant - sudo docker run --rm --privileged \ + docker run --rm --privileged \ -v ~/.docker:/root/.docker \ -v /run/docker.sock:/run/docker.sock:rw \ homeassistant/amd64-builder:$(versionBuilder) \ @@ -169,53 +170,51 @@ stages: steps: - template: templates/azp-step-ha-version.yaml@azure - script: | - mkdir -p ~/.docker - echo '{ "experimental": "enabled" }' > .docker/config.json - - sudo docker login -u $(dockerUser) -p $(dockerPassword) - displayName: 'Enable manifest / Docker login' + docker login -u $(dockerUser) -p $(dockerPassword) + displayName: 'Docker login' - script: | set -e + export DOCKER_CLI_EXPERIMENTAL=enabled function create_manifest() { local tag_l=$1 local tag_r=$2 - sudo docker --config .docker manifest create homeassistant/home-assistant:${tag_l} \ + docker manifest create homeassistant/home-assistant:${tag_l} \ homeassistant/amd64-homeassistant:${tag_r} \ homeassistant/i386-homeassistant:${tag_r} \ homeassistant/armhf-homeassistant:${tag_r} \ homeassistant/armv7-homeassistant:${tag_r} \ homeassistant/aarch64-homeassistant:${tag_r} - sudo docker --config .docker manifest annotate homeassistant/home-assistant:${tag_l} \ + docker manifest annotate homeassistant/home-assistant:${tag_l} \ homeassistant/amd64-homeassistant:${tag_r} \ --os linux --arch amd64 - sudo docker --config .docker manifest annotate homeassistant/home-assistant:${tag_l} \ + docker manifest annotate homeassistant/home-assistant:${tag_l} \ homeassistant/i386-homeassistant:${tag_r} \ --os linux --arch 386 - sudo docker --config .docker manifest annotate homeassistant/home-assistant:${tag_l} \ + docker manifest annotate homeassistant/home-assistant:${tag_l} \ homeassistant/armhf-homeassistant:${tag_r} \ --os linux --arch arm --variant=v6 - sudo docker --config .docker manifest annotate homeassistant/home-assistant:${tag_l} \ + docker manifest annotate homeassistant/home-assistant:${tag_l} \ homeassistant/armv7-homeassistant:${tag_r} \ --os linux --arch arm --variant=v7 - sudo docker --config .docker manifest annotate homeassistant/home-assistant:${tag_l} \ + docker manifest annotate homeassistant/home-assistant:${tag_l} \ homeassistant/aarch64-homeassistant:${tag_r} \ --os linux --arch arm64 --variant=v8 - sudo docker --config .docker manifest push --purge homeassistant/home-assistant:${tag_l} + docker manifest push --purge homeassistant/home-assistant:${tag_l} } - sudo docker pull homeassistant/amd64-homeassistant:$(homeassistantRelease) - sudo docker pull homeassistant/i386-homeassistant:$(homeassistantRelease) - sudo docker pull homeassistant/armhf-homeassistant:$(homeassistantRelease) - sudo docker pull homeassistant/armv7-homeassistant:$(homeassistantRelease) - sudo docker pull homeassistant/aarch64-homeassistant:$(homeassistantRelease) + docker pull homeassistant/amd64-homeassistant:$(homeassistantRelease) + docker pull homeassistant/i386-homeassistant:$(homeassistantRelease) + docker pull homeassistant/armhf-homeassistant:$(homeassistantRelease) + docker pull homeassistant/armv7-homeassistant:$(homeassistantRelease) + docker pull homeassistant/aarch64-homeassistant:$(homeassistantRelease) # Create version tag create_manifest "$(homeassistantRelease)" "$(homeassistantRelease)" From d7df61f9804844f32124c74f8210fd159b4f31d9 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 26 Aug 2019 21:12:49 +0200 Subject: [PATCH 267/273] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 896aaa710c1..7c88e615fa5 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -220,9 +220,9 @@ stages: create_manifest "$(homeassistantRelease)" "$(homeassistantRelease)" # Create general tags - if [[ "$version" =~ d ]]; then + if [[ "$(homeassistantRelease)" =~ d ]]; then create_manifest "dev" "$(homeassistantRelease)" - elif [[ "$version" =~ b ]]; then + elif [[ "$(homeassistantRelease)" =~ b ]]; then create_manifest "beta" "$(homeassistantRelease)" create_manifest "rc" "$(homeassistantRelease)" else From 907ffdb762fee4e34cec21cd54733f32c610882c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Aug 2019 12:45:13 -0700 Subject: [PATCH 268/273] Update translations --- .../adguard/.translations/es-419.json | 3 +- .../components/adguard/.translations/hr.json | 7 +++ .../components/adguard/.translations/id.json | 15 +++++++ .../ambiclimate/.translations/es-419.json | 13 +++++- .../components/auth/.translations/es-419.json | 6 ++- .../components/axis/.translations/es-419.json | 11 +++-- .../components/axis/.translations/fr.json | 3 +- .../components/cast/.translations/hr.json | 10 +++++ .../components/deconz/.translations/en.json | 13 +----- .../deconz/.translations/es-419.json | 6 ++- .../components/deconz/.translations/hr.json | 17 ++++++++ .../dialogflow/.translations/pl.json | 2 +- .../esphome/.translations/es-419.json | 2 + .../hangouts/.translations/es-419.json | 3 ++ .../components/heos/.translations/es-419.json | 3 ++ .../.translations/es-419.json | 3 +- .../homematicip_cloud/.translations/hr.json | 7 +++ .../components/hue/.translations/es-419.json | 1 + .../components/hue/.translations/hr.json | 16 +++++++ .../components/ifttt/.translations/hr.json | 5 +++ .../iqvia/.translations/es-419.json | 14 ++++++ .../components/iqvia/.translations/id.json | 11 +++++ .../life360/.translations/es-419.json | 5 +++ .../components/life360/.translations/fr.json | 4 ++ .../components/life360/.translations/hr.json | 24 +++++++++++ .../components/life360/.translations/id.json | 7 +++ .../components/life360/.translations/pl.json | 8 ++-- .../logi_circle/.translations/es-419.json | 27 ++++++++++++ .../components/met/.translations/es-419.json | 20 +++++++++ .../components/met/.translations/hr.json | 20 +++++++++ .../components/met/.translations/id.json | 13 ++++++ .../components/met/.translations/pl.json | 2 +- .../mobile_app/.translations/es-419.json | 3 ++ .../moon/.translations/sensor.es-419.json | 7 +-- .../components/mqtt/.translations/hr.json | 15 +++++++ .../components/nest/.translations/hr.json | 21 +++++++++ .../notion/.translations/es-419.json | 18 ++++++++ .../components/notion/.translations/hr.json | 19 ++++++++ .../components/notion/.translations/pl.json | 4 +- .../onboarding/.translations/es-419.json | 7 +++ .../onboarding/.translations/id.json | 5 +++ .../components/openuv/.translations/hr.json | 13 ++++++ .../plaato/.translations/es-419.json | 18 ++++++++ .../components/plaato/.translations/hr.json | 18 ++++++++ .../components/plaato/.translations/pl.json | 2 +- .../point/.translations/es-419.json | 2 + .../components/ps4/.translations/es-419.json | 6 +++ .../season/.translations/sensor.es-419.json | 3 +- .../sensor/.translations/season.hr.json | 8 ++++ .../components/somfy/.translations/fr.json | 5 +++ .../components/somfy/.translations/hr.json | 8 ++++ .../components/sonos/.translations/hr.json | 10 +++++ .../tplink/.translations/es-419.json | 5 +++ .../components/traccar/.translations/en.json | 18 ++++++++ .../tradfri/.translations/es-419.json | 4 +- .../components/tradfri/.translations/hr.json | 15 +++++++ .../components/tradfri/.translations/pl.json | 2 +- .../components/unifi/.translations/en.json | 43 ++++++------------- .../components/unifi/.translations/hr.json | 14 ++++++ .../components/upnp/.translations/hr.json | 9 ++++ .../components/wemo/.translations/es-419.json | 15 +++++++ .../components/wemo/.translations/hr.json | 5 +++ .../wwlln/.translations/es-419.json | 18 ++++++++ .../components/wwlln/.translations/hr.json | 18 ++++++++ .../components/wwlln/.translations/pl.json | 6 +-- .../components/zone/.translations/hr.json | 21 +++++++++ .../zwave/.translations/es-419.json | 3 ++ 67 files changed, 617 insertions(+), 72 deletions(-) create mode 100644 homeassistant/components/adguard/.translations/hr.json create mode 100644 homeassistant/components/adguard/.translations/id.json create mode 100644 homeassistant/components/cast/.translations/hr.json create mode 100644 homeassistant/components/deconz/.translations/hr.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/hr.json create mode 100644 homeassistant/components/hue/.translations/hr.json create mode 100644 homeassistant/components/ifttt/.translations/hr.json create mode 100644 homeassistant/components/iqvia/.translations/es-419.json create mode 100644 homeassistant/components/iqvia/.translations/id.json create mode 100644 homeassistant/components/life360/.translations/es-419.json create mode 100644 homeassistant/components/life360/.translations/hr.json create mode 100644 homeassistant/components/life360/.translations/id.json create mode 100644 homeassistant/components/logi_circle/.translations/es-419.json create mode 100644 homeassistant/components/met/.translations/es-419.json create mode 100644 homeassistant/components/met/.translations/hr.json create mode 100644 homeassistant/components/met/.translations/id.json create mode 100644 homeassistant/components/mqtt/.translations/hr.json create mode 100644 homeassistant/components/nest/.translations/hr.json create mode 100644 homeassistant/components/notion/.translations/es-419.json create mode 100644 homeassistant/components/notion/.translations/hr.json create mode 100644 homeassistant/components/onboarding/.translations/es-419.json create mode 100644 homeassistant/components/onboarding/.translations/id.json create mode 100644 homeassistant/components/openuv/.translations/hr.json create mode 100644 homeassistant/components/plaato/.translations/es-419.json create mode 100644 homeassistant/components/plaato/.translations/hr.json create mode 100644 homeassistant/components/sensor/.translations/season.hr.json create mode 100644 homeassistant/components/somfy/.translations/hr.json create mode 100644 homeassistant/components/sonos/.translations/hr.json create mode 100644 homeassistant/components/traccar/.translations/en.json create mode 100644 homeassistant/components/tradfri/.translations/hr.json create mode 100644 homeassistant/components/unifi/.translations/hr.json create mode 100644 homeassistant/components/upnp/.translations/hr.json create mode 100644 homeassistant/components/wemo/.translations/es-419.json create mode 100644 homeassistant/components/wemo/.translations/hr.json create mode 100644 homeassistant/components/wwlln/.translations/es-419.json create mode 100644 homeassistant/components/wwlln/.translations/hr.json create mode 100644 homeassistant/components/zone/.translations/hr.json diff --git a/homeassistant/components/adguard/.translations/es-419.json b/homeassistant/components/adguard/.translations/es-419.json index d62402f2eee..ed8e0c3a358 100644 --- a/homeassistant/components/adguard/.translations/es-419.json +++ b/homeassistant/components/adguard/.translations/es-419.json @@ -20,7 +20,8 @@ "username": "Nombre de usuario", "verify_ssl": "AdGuard Home utiliza un certificado adecuado" }, - "description": "Configure su instancia de AdGuard Home para permitir la supervisi\u00f3n y el control." + "description": "Configure su instancia de AdGuard Home para permitir la supervisi\u00f3n y el control.", + "title": "Enlace su AdGuard Home." } }, "title": "AdGuard Home" diff --git a/homeassistant/components/adguard/.translations/hr.json b/homeassistant/components/adguard/.translations/hr.json new file mode 100644 index 00000000000..869cc46ea10 --- /dev/null +++ b/homeassistant/components/adguard/.translations/hr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "Postoje\u0107a konfiguracija je a\u017eurirana." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/id.json b/homeassistant/components/adguard/.translations/id.json new file mode 100644 index 00000000000..3548361e396 --- /dev/null +++ b/homeassistant/components/adguard/.translations/id.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "connection_error": "Gagal terhubung." + }, + "step": { + "user": { + "data": { + "password": "Kata sandi", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/.translations/es-419.json b/homeassistant/components/ambiclimate/.translations/es-419.json index eaac252d605..607454f4402 100644 --- a/homeassistant/components/ambiclimate/.translations/es-419.json +++ b/homeassistant/components/ambiclimate/.translations/es-419.json @@ -7,6 +7,17 @@ }, "create_entry": { "default": "Autenticaci\u00f3n exitosa con Ambiclimate" - } + }, + "error": { + "follow_link": "Por favor, siga el enlace y autent\u00edquese antes de presionar Enviar", + "no_token": "No autenticado con Ambiclimate" + }, + "step": { + "auth": { + "description": "Por favor, siga este [link]('authorization_url') y Permitir acceso a su cuenta de Ambiclimate, luego vuelva y presione Enviar a continuaci\u00f3n.\n(Aseg\u00farese de que la url de devoluci\u00f3n de llamada especificada es {cb_url})", + "title": "Autenticaci\u00f3n de Ambiclimate" + } + }, + "title": "Ambiclimate" } } \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/es-419.json b/homeassistant/components/auth/.translations/es-419.json index 852965596e0..4ac97068905 100644 --- a/homeassistant/components/auth/.translations/es-419.json +++ b/homeassistant/components/auth/.translations/es-419.json @@ -16,9 +16,13 @@ "description": "Se ha enviado una contrase\u00f1a \u00fanica a trav\u00e9s de **notify.{notify_service}**. Por favor ingr\u00e9selo a continuaci\u00f3n:", "title": "Verificar la configuracion" } - } + }, + "title": "Notificar contrase\u00f1a de un solo uso" }, "totp": { + "error": { + "invalid_code": "C\u00f3digo no v\u00e1lido, por favor vuelva a intentarlo. Si recibe este error constantemente, aseg\u00farese de que el reloj de su sistema Home Assistant sea exacto." + }, "step": { "init": { "description": "Para activar la autenticaci\u00f3n de dos factores utilizando contrase\u00f1as de un solo uso basadas en el tiempo, escanee el c\u00f3digo QR con su aplicaci\u00f3n de autenticaci\u00f3n. Si no tiene uno, le recomendamos [Autenticador de Google] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \n Despu\u00e9s de escanear el c\u00f3digo, ingrese el c\u00f3digo de seis d\u00edgitos de su aplicaci\u00f3n para verificar la configuraci\u00f3n. Si tiene problemas para escanear el c\u00f3digo QR, realice una configuraci\u00f3n manual con el c\u00f3digo ** ` {code} ` **.", diff --git a/homeassistant/components/axis/.translations/es-419.json b/homeassistant/components/axis/.translations/es-419.json index 1e9301a19da..c5404a173f6 100644 --- a/homeassistant/components/axis/.translations/es-419.json +++ b/homeassistant/components/axis/.translations/es-419.json @@ -2,10 +2,13 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "bad_config_file": "Datos err\u00f3neos del archivo de configuraci\u00f3n" + "bad_config_file": "Datos err\u00f3neos del archivo de configuraci\u00f3n", + "link_local_address": "Las direcciones locales de enlace no son compatibles", + "not_axis_device": "El dispositivo descubierto no es un dispositivo de Axis" }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ya est\u00e1 en progreso.", "device_unavailable": "El dispositivo no est\u00e1 disponible", "faulty_credentials": "Credenciales de usuario incorrectas" }, @@ -15,8 +18,10 @@ "password": "Contrase\u00f1a", "port": "Puerto", "username": "Nombre de usuario" - } + }, + "title": "Configurar dispositivo Axis" } - } + }, + "title": "Dispositivo Axis" } } \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/fr.json b/homeassistant/components/axis/.translations/fr.json index 020cd8f5946..e85fceaf463 100644 --- a/homeassistant/components/axis/.translations/fr.json +++ b/homeassistant/components/axis/.translations/fr.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "bad_config_file": "Mauvaises donn\u00e9es du fichier de configuration", - "link_local_address": "Les adresses locales ne sont pas prises en charge" + "link_local_address": "Les adresses locales ne sont pas prises en charge", + "not_axis_device": "L'appareil d\u00e9couvert n'est pas un appareil Axis" }, "error": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", diff --git a/homeassistant/components/cast/.translations/hr.json b/homeassistant/components/cast/.translations/hr.json new file mode 100644 index 00000000000..91dafab0201 --- /dev/null +++ b/homeassistant/components/cast/.translations/hr.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 34da602a6ce..dd8f1cc4026 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -40,16 +40,5 @@ } }, "title": "deCONZ Zigbee gateway" - }, - "options": { - "step": { - "deconz_devices": { - "description": "Configure visibility of deCONZ device types", - "data": { - "allow_clip_sensor": "Allow deCONZ CLIP sensors", - "allow_deconz_groups": "Allow deCONZ light groups" - } - } - } } -} +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/es-419.json b/homeassistant/components/deconz/.translations/es-419.json index 4ae633ef165..1a5d992ef7b 100644 --- a/homeassistant/components/deconz/.translations/es-419.json +++ b/homeassistant/components/deconz/.translations/es-419.json @@ -2,7 +2,9 @@ "config": { "abort": { "already_configured": "El Bridge ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n para el puente ya est\u00e1 en progreso.", "no_bridges": "No se descubrieron puentes deCONZ", + "not_deconz_bridge": "No es un puente deCONZ", "one_instance_only": "El componente solo admite una instancia deCONZ" }, "error": { @@ -13,7 +15,8 @@ "data": { "allow_clip_sensor": "Permitir la importaci\u00f3n de sensores virtuales", "allow_deconz_groups": "Permitir la importaci\u00f3n de grupos deCONZ" - } + }, + "description": "\u00bfDesea configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento hass.io {addon}?" }, "init": { "data": { @@ -23,6 +26,7 @@ "title": "Definir el gateway deCONZ" }, "link": { + "description": "Desbloquee su puerta de enlace deCONZ para registrarse con Home Assistant. \n\n 1. Vaya a Configuraci\u00f3n deCONZ - > Gateway - > Avanzado \n 2. Presione el bot\u00f3n \"Autenticar aplicaci\u00f3n\"", "title": "Enlazar con deCONZ" }, "options": { diff --git a/homeassistant/components/deconz/.translations/hr.json b/homeassistant/components/deconz/.translations/hr.json new file mode 100644 index 00000000000..2f2eb6df214 --- /dev/null +++ b/homeassistant/components/deconz/.translations/hr.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "init": { + "data": { + "host": "Host", + "port": "Port" + } + }, + "options": { + "data": { + "allow_clip_sensor": "Dopusti uvoz virtualnih senzora" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/.translations/pl.json b/homeassistant/components/dialogflow/.translations/pl.json index 3395b31b4c7..ee222c83b51 100644 --- a/homeassistant/components/dialogflow/.translations/pl.json +++ b/homeassistant/components/dialogflow/.translations/pl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Wymagana jest tylko jedna instancja." }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 [Dialogflow Webhook]({twilio_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 [Dialogflow Webhook]({dialogflow_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." }, "step": { "user": { diff --git a/homeassistant/components/esphome/.translations/es-419.json b/homeassistant/components/esphome/.translations/es-419.json index 58dbba34fa8..a0a2d77d48c 100644 --- a/homeassistant/components/esphome/.translations/es-419.json +++ b/homeassistant/components/esphome/.translations/es-419.json @@ -8,6 +8,7 @@ "invalid_password": "\u00a1Contrase\u00f1a invalida!", "resolve_error": "No se puede resolver la direcci\u00f3n de la ESP. Si este error persiste, configure una direcci\u00f3n IP est\u00e1tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, + "flow_title": "ESPHome: {name}", "step": { "authenticate": { "data": { @@ -17,6 +18,7 @@ "title": "Escriba la contrase\u00f1a" }, "discovery_confirm": { + "description": "\u00bfDesea agregar el nodo ESPHome `{name}` a Home Assistant?", "title": "Nodo ESPHome descubierto" }, "user": { diff --git a/homeassistant/components/hangouts/.translations/es-419.json b/homeassistant/components/hangouts/.translations/es-419.json index 951a30f1826..3a297eb15ea 100644 --- a/homeassistant/components/hangouts/.translations/es-419.json +++ b/homeassistant/components/hangouts/.translations/es-419.json @@ -9,6 +9,9 @@ }, "step": { "2fa": { + "data": { + "2fa": "Pin 2FA" + }, "title": "Autenticaci\u00f3n de 2 factores" }, "user": { diff --git a/homeassistant/components/heos/.translations/es-419.json b/homeassistant/components/heos/.translations/es-419.json index 12ed8cc457a..66c02884a7e 100644 --- a/homeassistant/components/heos/.translations/es-419.json +++ b/homeassistant/components/heos/.translations/es-419.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_setup": "Solo puede configurar una sola conexi\u00f3n Heos, ya que ser\u00e1 compatible con todos los dispositivos de la red." + }, "title": "Heos" } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/es-419.json b/homeassistant/components/homekit_controller/.translations/es-419.json index b058e94e25a..9ddf336c060 100644 --- a/homeassistant/components/homekit_controller/.translations/es-419.json +++ b/homeassistant/components/homekit_controller/.translations/es-419.json @@ -15,6 +15,7 @@ "device": "Dispositivo" } } - } + }, + "title": "Accesorio HomeKit" } } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/hr.json b/homeassistant/components/homematicip_cloud/.translations/hr.json new file mode 100644 index 00000000000..648dbfe73f9 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/hr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "unknown": "Do\u0161lo je do nepoznate pogre\u0161ke." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/es-419.json b/homeassistant/components/hue/.translations/es-419.json index 8efc9101d9a..48a2ff233da 100644 --- a/homeassistant/components/hue/.translations/es-419.json +++ b/homeassistant/components/hue/.translations/es-419.json @@ -6,6 +6,7 @@ "cannot_connect": "No se puede conectar al puente", "discover_timeout": "Incapaz de descubrir puentes Hue", "no_bridges": "No se descubrieron puentes Philips Hue", + "not_hue_bridge": "No es un puente Hue", "unknown": "Se produjo un error desconocido" }, "error": { diff --git a/homeassistant/components/hue/.translations/hr.json b/homeassistant/components/hue/.translations/hr.json new file mode 100644 index 00000000000..16a1b19ff8e --- /dev/null +++ b/homeassistant/components/hue/.translations/hr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "linking": "Do\u0161lo je do nepoznate pogre\u0161ke u povezivanju.", + "register_failed": "Registracija nije uspjela. Poku\u0161ajte ponovo" + }, + "step": { + "init": { + "data": { + "host": "Host" + } + } + }, + "title": "Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/hr.json b/homeassistant/components/ifttt/.translations/hr.json new file mode 100644 index 00000000000..077956287b3 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/hr.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/es-419.json b/homeassistant/components/iqvia/.translations/es-419.json new file mode 100644 index 00000000000..b107e1bb696 --- /dev/null +++ b/homeassistant/components/iqvia/.translations/es-419.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_zip_code": "El c\u00f3digo postal no es v\u00e1lido" + }, + "step": { + "user": { + "data": { + "zip_code": "C\u00f3digo postal" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/id.json b/homeassistant/components/iqvia/.translations/id.json new file mode 100644 index 00000000000..a93f9aac26f --- /dev/null +++ b/homeassistant/components/iqvia/.translations/id.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zip_code": "Kode Pos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/es-419.json b/homeassistant/components/life360/.translations/es-419.json new file mode 100644 index 00000000000..3f9bfab3304 --- /dev/null +++ b/homeassistant/components/life360/.translations/es-419.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Life360" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/fr.json b/homeassistant/components/life360/.translations/fr.json index 95df1c991a2..cb4682fc937 100644 --- a/homeassistant/components/life360/.translations/fr.json +++ b/homeassistant/components/life360/.translations/fr.json @@ -4,6 +4,9 @@ "invalid_credentials": "Informations d'identification invalides", "user_already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9" }, + "create_entry": { + "default": "Pour d\u00e9finir les options avanc\u00e9es, voir [Documentation de Life360]( {docs_url} )." + }, "error": { "invalid_credentials": "Informations d'identification invalides", "invalid_username": "Nom d'utilisateur invalide", @@ -15,6 +18,7 @@ "password": "Mot de passe", "username": "Nom d'utilisateur" }, + "description": "Pour d\u00e9finir des options avanc\u00e9es, voir [Documentation Life360]({docs_url}).\nVous pouvez le faire avant d'ajouter des comptes.", "title": "Informations sur le compte Life360" } }, diff --git a/homeassistant/components/life360/.translations/hr.json b/homeassistant/components/life360/.translations/hr.json new file mode 100644 index 00000000000..5cf8cbef17f --- /dev/null +++ b/homeassistant/components/life360/.translations/hr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "invalid_credentials": "Neva\u017ee\u0107e vjerodajnice", + "user_already_configured": "Ra\u010dun je ve\u0107 konfiguriran" + }, + "create_entry": { + "default": "Da biste postavili napredne opcije, pogledajte [Life360 dokumentacija] ( {docs_url} )." + }, + "error": { + "invalid_credentials": "Neva\u017ee\u0107e vjerodajnice", + "invalid_username": "Neispravno korisni\u010dko ime", + "user_already_configured": "Ra\u010dun je ve\u0107 konfiguriran" + }, + "step": { + "user": { + "data": { + "password": "Lozinka", + "username": "Korisni\u010dko ime" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/id.json b/homeassistant/components/life360/.translations/id.json new file mode 100644 index 00000000000..2bb7a1cca68 --- /dev/null +++ b/homeassistant/components/life360/.translations/id.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_username": "Nama pengguna tidak valid" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/pl.json b/homeassistant/components/life360/.translations/pl.json index b1523da188c..15aabaa6308 100644 --- a/homeassistant/components/life360/.translations/pl.json +++ b/homeassistant/components/life360/.translations/pl.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "invalid_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce", - "user_already_configured": "Konto jest ju\u017c skonfigurowane." + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", + "user_already_configured": "Konto zosta\u0142o ju\u017c skonfigurowane." }, "create_entry": { "default": "Aby skonfigurowa\u0107 zaawansowane ustawienia, zapoznaj si\u0119 z [dokumentacj\u0105 Life360]({docs_url})." }, "error": { - "invalid_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce", + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", "invalid_username": "Nieprawid\u0142owa nazwa u\u017cytkownika", - "user_already_configured": "Konto jest ju\u017c skonfigurowane." + "user_already_configured": "Konto zosta\u0142o ju\u017c skonfigurowane." }, "step": { "user": { diff --git a/homeassistant/components/logi_circle/.translations/es-419.json b/homeassistant/components/logi_circle/.translations/es-419.json new file mode 100644 index 00000000000..2393908e281 --- /dev/null +++ b/homeassistant/components/logi_circle/.translations/es-419.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_setup": "Solo puede configurar una sola cuenta de Logi Circle.", + "external_error": "Se produjo una excepci\u00f3n de otro flujo.", + "external_setup": "Logi Circle se configur\u00f3 correctamente desde otro flujo." + }, + "create_entry": { + "default": "Autenticado con \u00e9xito con Logi Circle." + }, + "error": { + "auth_error": "Autorizaci\u00f3n de API fallida." + }, + "step": { + "auth": { + "title": "Autenticar con Logi Circle" + }, + "user": { + "data": { + "flow_impl": "Proveedor" + }, + "title": "Proveedor de autenticaci\u00f3n" + } + }, + "title": "Logi Circle" + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/es-419.json b/homeassistant/components/met/.translations/es-419.json new file mode 100644 index 00000000000..d744de150d2 --- /dev/null +++ b/homeassistant/components/met/.translations/es-419.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "name_exists": "El nombre ya existe" + }, + "step": { + "user": { + "data": { + "elevation": "Elevaci\u00f3n", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre" + }, + "description": "Meteorologisk institutt", + "title": "Ubicaci\u00f3n" + } + }, + "title": "Met.no" + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/hr.json b/homeassistant/components/met/.translations/hr.json new file mode 100644 index 00000000000..6505229355c --- /dev/null +++ b/homeassistant/components/met/.translations/hr.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "name_exists": "Ime ve\u0107 postoji" + }, + "step": { + "user": { + "data": { + "elevation": "Elevacija", + "latitude": "Zemljopisna \u0161irina", + "longitude": "Zemljopisna du\u017eina", + "name": "Ime" + }, + "description": "Meteorolo\u0161ki institutt", + "title": "Lokacija" + } + }, + "title": "Met.no" + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/id.json b/homeassistant/components/met/.translations/id.json new file mode 100644 index 00000000000..12854e4ed61 --- /dev/null +++ b/homeassistant/components/met/.translations/id.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "elevation": "Ketinggian", + "name": "Nama" + }, + "title": "Lokasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/pl.json b/homeassistant/components/met/.translations/pl.json index 61b66b794e1..d44142213bf 100644 --- a/homeassistant/components/met/.translations/pl.json +++ b/homeassistant/components/met/.translations/pl.json @@ -11,7 +11,7 @@ "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "name": "Nazwa" }, - "description": "Meteorologisk institutt", + "description": "Instytut Meteorologiczny", "title": "Lokalizacja" } }, diff --git a/homeassistant/components/mobile_app/.translations/es-419.json b/homeassistant/components/mobile_app/.translations/es-419.json index 417d0627616..271e38147c3 100644 --- a/homeassistant/components/mobile_app/.translations/es-419.json +++ b/homeassistant/components/mobile_app/.translations/es-419.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "install_app": "Abra la aplicaci\u00f3n m\u00f3vil para configurar la integraci\u00f3n con Home Assistant. Consulte [los documentos] ({apps_url}) para obtener una lista de aplicaciones compatibles." + }, "step": { "confirm": { "title": "Aplicaci\u00f3n movil" diff --git a/homeassistant/components/moon/.translations/sensor.es-419.json b/homeassistant/components/moon/.translations/sensor.es-419.json index 71cfab736cb..89823dd2055 100644 --- a/homeassistant/components/moon/.translations/sensor.es-419.json +++ b/homeassistant/components/moon/.translations/sensor.es-419.json @@ -2,11 +2,6 @@ "state": { "first_quarter": "Cuarto creciente", "full_moon": "Luna llena", - "last_quarter": "Cuarto menguante", - "new_moon": "Luna nueva", - "waning_crescent": "Luna menguante", - "waning_gibbous": "Luna menguante gibosa", - "waxing_crescent": "Luna creciente", - "waxing_gibbous": "Luna creciente gibosa" + "last_quarter": "Cuarto menguante" } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/hr.json b/homeassistant/components/mqtt/.translations/hr.json new file mode 100644 index 00000000000..b3c82fdd8db --- /dev/null +++ b/homeassistant/components/mqtt/.translations/hr.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "broker": { + "data": { + "password": "Lozinka", + "port": "Port", + "username": "Korisni\u010dko ime" + }, + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/hr.json b/homeassistant/components/nest/.translations/hr.json new file mode 100644 index 00000000000..b96a358f2f0 --- /dev/null +++ b/homeassistant/components/nest/.translations/hr.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "invalid_code": "Neispravan kod" + }, + "step": { + "init": { + "data": { + "flow_impl": "Pru\u017eatelj usluge" + }, + "title": "Pru\u017eatelj usluge autentifikacije" + }, + "link": { + "data": { + "code": "PIN kod" + } + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/es-419.json b/homeassistant/components/notion/.translations/es-419.json new file mode 100644 index 00000000000..ad2f19b0668 --- /dev/null +++ b/homeassistant/components/notion/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_credentials": "Nombre de usuario o contrase\u00f1a inv\u00e1lidos", + "no_devices": "No se han encontrado dispositivos en la cuenta." + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario/direcci\u00f3n de correo electr\u00f3nico" + }, + "title": "Complete su informaci\u00f3n" + } + }, + "title": "Noci\u00f3n" + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/hr.json b/homeassistant/components/notion/.translations/hr.json new file mode 100644 index 00000000000..b20317a236a --- /dev/null +++ b/homeassistant/components/notion/.translations/hr.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Korisni\u010dko ime je ve\u0107 registrirano", + "invalid_credentials": "Neispravno korisni\u010dko ime ili lozinka", + "no_devices": "Nisu prona\u0111eni ure\u0111aji na ra\u010dunu" + }, + "step": { + "user": { + "data": { + "password": "Lozinka", + "username": "Korisni\u010dko ime/adresa e-po\u0161te" + }, + "title": "Ispunite svoje podatke" + } + }, + "title": "Pojam" + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/pl.json b/homeassistant/components/notion/.translations/pl.json index 0c1fe674887..c35de9c535c 100644 --- a/homeassistant/components/notion/.translations/pl.json +++ b/homeassistant/components/notion/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "Nazwa u\u017cytkownika jest ju\u017c zarejestrowana", + "identifier_exists": "Nazwa u\u017cytkownika ju\u017c zarejestrowana", "invalid_credentials": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o", "no_devices": "Nie znaleziono urz\u0105dze\u0144 na koncie" }, @@ -14,6 +14,6 @@ "title": "Wprowad\u017a swoje dane" } }, - "title": "Notion" + "title": "Poj\u0119cie" } } \ No newline at end of file diff --git a/homeassistant/components/onboarding/.translations/es-419.json b/homeassistant/components/onboarding/.translations/es-419.json new file mode 100644 index 00000000000..747074436d7 --- /dev/null +++ b/homeassistant/components/onboarding/.translations/es-419.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "Habitaci\u00f3n", + "kitchen": "Cocina", + "living_room": "Sala" + } +} \ No newline at end of file diff --git a/homeassistant/components/onboarding/.translations/id.json b/homeassistant/components/onboarding/.translations/id.json new file mode 100644 index 00000000000..33e8a88a9ae --- /dev/null +++ b/homeassistant/components/onboarding/.translations/id.json @@ -0,0 +1,5 @@ +{ + "area": { + "kitchen": "Dapur" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/hr.json b/homeassistant/components/openuv/.translations/hr.json new file mode 100644 index 00000000000..835929d26df --- /dev/null +++ b/homeassistant/components/openuv/.translations/hr.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "elevation": "Elevacija", + "latitude": "Zemljopisna \u0161irina", + "longitude": "Zemljopisna du\u017eina" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/.translations/es-419.json b/homeassistant/components/plaato/.translations/es-419.json new file mode 100644 index 00000000000..d63802984ef --- /dev/null +++ b/homeassistant/components/plaato/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes de Plaato Airlock.", + "one_instance_allowed": "Solo una instancia es necesaria." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, deber\u00e1 configurar la funci\u00f3n de webhook en Plaato Airlock. \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: `{webhook_url}` \n - M\u00e9todo: POST \n\n Consulte [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s detalles." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1 seguro de que deseas configurar Plaato Airlock?", + "title": "Configurar el Webhook de Plaato" + } + }, + "title": "Plaato Airlock" + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/.translations/hr.json b/homeassistant/components/plaato/.translations/hr.json new file mode 100644 index 00000000000..680571040b1 --- /dev/null +++ b/homeassistant/components/plaato/.translations/hr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Va\u0161 Home Assistant mora biti dostupan s interneta za primanje poruka od Plaato Airlocka.", + "one_instance_allowed": "Potrebna je samo jedna instanca." + }, + "create_entry": { + "default": "Za slanje doga\u0111aja kod ku\u0107nog pomo\u0107nika, morat \u0107ete postaviti zna\u010dajku webhook u Plaato Airlock.\n\nIspunite sljede\u0107e informacije:\n\n-URL: ' {webhook_url} '\n-Metoda: POST\n\nZa dodatne detalje pogledajte [dokumentaciju] ({docs_url})." + }, + "step": { + "user": { + "description": "Jeste li sigurni da \u017eelite postaviti Plaato Airlock?", + "title": "Postavljanje Plaato Webhook" + } + }, + "title": "Plaato Airlock" + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/.translations/pl.json b/homeassistant/components/plaato/.translations/pl.json index aa7eb5f29bc..aac48ee4774 100644 --- a/homeassistant/components/plaato/.translations/pl.json +++ b/homeassistant/components/plaato/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Czy chcesz skonfigurowa\u0107 Plaato Airlock?", + "description": "Czy na pewno chcesz skonfigurowa\u0107 Airlock Plaato?", "title": "Konfiguracja Plaato Webhook" } }, diff --git a/homeassistant/components/point/.translations/es-419.json b/homeassistant/components/point/.translations/es-419.json index c20e3350272..7436513ba6f 100644 --- a/homeassistant/components/point/.translations/es-419.json +++ b/homeassistant/components/point/.translations/es-419.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_setup": "Solo puede configurar una cuenta Point.", + "authorize_url_fail": "Error desconocido al generar una URL de autorizaci\u00f3n.", "external_setup": "Punto configurado con \u00e9xito desde otro flujo." }, "error": { diff --git a/homeassistant/components/ps4/.translations/es-419.json b/homeassistant/components/ps4/.translations/es-419.json index 093ee552951..0f7066df007 100644 --- a/homeassistant/components/ps4/.translations/es-419.json +++ b/homeassistant/components/ps4/.translations/es-419.json @@ -25,6 +25,12 @@ }, "description": "Ingresa tu informaci\u00f3n de PlayStation 4. Para 'PIN', navegue hasta 'Configuraci\u00f3n' en su consola PlayStation 4. Luego navegue a 'Configuraci\u00f3n de conexi\u00f3n de la aplicaci\u00f3n m\u00f3vil' y seleccione 'Agregar dispositivo'. Ingrese el PIN que se muestra.", "title": "Playstation 4" + }, + "mode": { + "data": { + "mode": "Modo de configuraci\u00f3n" + }, + "title": "Playstation 4" } }, "title": "Playstation 4" diff --git a/homeassistant/components/season/.translations/sensor.es-419.json b/homeassistant/components/season/.translations/sensor.es-419.json index 65df6a58b10..09ad22740cd 100644 --- a/homeassistant/components/season/.translations/sensor.es-419.json +++ b/homeassistant/components/season/.translations/sensor.es-419.json @@ -2,7 +2,6 @@ "state": { "autumn": "Oto\u00f1o", "spring": "Primavera", - "summer": "Verano", - "winter": "Invierno" + "summer": "Verano" } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.hr.json b/homeassistant/components/sensor/.translations/season.hr.json new file mode 100644 index 00000000000..ff36d1ca66b --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.hr.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Jesen", + "spring": "Prolje\u0107e", + "summer": "Ljeto", + "winter": "Zima" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/fr.json b/homeassistant/components/somfy/.translations/fr.json index 6afb01169cb..ba873c4f029 100644 --- a/homeassistant/components/somfy/.translations/fr.json +++ b/homeassistant/components/somfy/.translations/fr.json @@ -1,5 +1,10 @@ { "config": { + "abort": { + "already_setup": "Vous ne pouvez configurer qu'un seul compte Somfy.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration d'url autoriser.", + "missing_configuration": "Le composant Somfy n'est pas configur\u00e9. Veuillez suivre la documentation." + }, "create_entry": { "default": "Authentifi\u00e9 avec succ\u00e8s avec Somfy." }, diff --git a/homeassistant/components/somfy/.translations/hr.json b/homeassistant/components/somfy/.translations/hr.json new file mode 100644 index 00000000000..3a904102076 --- /dev/null +++ b/homeassistant/components/somfy/.translations/hr.json @@ -0,0 +1,8 @@ +{ + "config": { + "create_entry": { + "default": "Uspje\u0161no autentificirano sa Somfy." + }, + "title": "Somfy" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/hr.json b/homeassistant/components/sonos/.translations/hr.json new file mode 100644 index 00000000000..c91f9a78c29 --- /dev/null +++ b/homeassistant/components/sonos/.translations/hr.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/es-419.json b/homeassistant/components/tplink/.translations/es-419.json index 1d9fb41fc8c..2832804113a 100644 --- a/homeassistant/components/tplink/.translations/es-419.json +++ b/homeassistant/components/tplink/.translations/es-419.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos TP-Link en la red.", + "single_instance_allowed": "Solo es necesaria una \u00fanica configuraci\u00f3n." + }, "step": { "confirm": { + "description": "\u00bfDesea configurar dispositivos inteligentes TP-Link?", "title": "TP-Link Smart Home" } }, diff --git a/homeassistant/components/traccar/.translations/en.json b/homeassistant/components/traccar/.translations/en.json new file mode 100644 index 00000000000..a8804835278 --- /dev/null +++ b/homeassistant/components/traccar/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Traccar.", + "one_instance_allowed": "Only a single instance is necessary." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to setup the webhook feature in Traccar.\n\nUse the following url: `{webhook_url}`\n\nSee [the documentation]({docs_url}) for further details." + }, + "step": { + "user": { + "description": "Are you sure you want to set up Traccar?", + "title": "Set up Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/es-419.json b/homeassistant/components/tradfri/.translations/es-419.json index 55016606e2d..4b3e1ed52d4 100644 --- a/homeassistant/components/tradfri/.translations/es-419.json +++ b/homeassistant/components/tradfri/.translations/es-419.json @@ -1,9 +1,11 @@ { "config": { "abort": { - "already_configured": "El Bridge ya est\u00e1 configurado" + "already_configured": "El Bridge ya est\u00e1 configurado", + "already_in_progress": "La configuraci\u00f3n del puente ya est\u00e1 en progreso." }, "error": { + "cannot_connect": "No se puede conectar a la puerta de enlace.", "invalid_key": "Error al registrarse con la clave proporcionada. Si esto sigue sucediendo, intente reiniciar el gateway.", "timeout": "Tiempo de espera para validar el c\u00f3digo." }, diff --git a/homeassistant/components/tradfri/.translations/hr.json b/homeassistant/components/tradfri/.translations/hr.json new file mode 100644 index 00000000000..b9b9cc6c0eb --- /dev/null +++ b/homeassistant/components/tradfri/.translations/hr.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfiguracija premosnice je ve\u0107 u tijeku." + }, + "step": { + "auth": { + "data": { + "host": "Host" + } + } + }, + "title": "IKEA TR\u00c5DFRI" + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/pl.json b/homeassistant/components/tradfri/.translations/pl.json index a61a028f396..e3fcfc89c5b 100644 --- a/homeassistant/components/tradfri/.translations/pl.json +++ b/homeassistant/components/tradfri/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Mostek jest ju\u017c skonfigurowany", - "already_in_progress": "Konfigurowanie mostka jest ju\u017c w toku." + "already_in_progress": "Konfiguracja mostka jest ju\u017c w toku." }, "error": { "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z bram\u0105.", diff --git a/homeassistant/components/unifi/.translations/en.json b/homeassistant/components/unifi/.translations/en.json index c484bfbf09f..3686148fdb6 100644 --- a/homeassistant/components/unifi/.translations/en.json +++ b/homeassistant/components/unifi/.translations/en.json @@ -1,41 +1,26 @@ { "config": { - "title": "UniFi Controller", - "step": { - "user": { - "title": "Set up UniFi Controller", - "data": { - "host": "Host", - "username": "User name", - "password": "Password", - "port": "Port", - "site": "Site ID", - "verify_ssl": "Controller using proper certificate" - } - } + "abort": { + "already_configured": "Controller site is already configured", + "user_privilege": "User needs to be administrator" }, "error": { "faulty_credentials": "Bad user credentials", "service_unavailable": "No service available" }, - "abort": { - "already_configured": "Controller site is already configured", - "user_privilege": "User needs to be administrator" - } - }, - "options": { "step": { - "init": { - "data": {} - }, - "device_tracker": { + "user": { "data": { - "detection_time": "Time in seconds from last seen until considered away", - "track_clients": "Track network clients", - "track_devices": "Track network devices (Ubiquiti devices)", - "track_wired_clients": "Include wired network clients" - } + "host": "Host", + "password": "Password", + "port": "Port", + "site": "Site ID", + "username": "User name", + "verify_ssl": "Controller using proper certificate" + }, + "title": "Set up UniFi Controller" } - } + }, + "title": "UniFi Controller" } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/hr.json b/homeassistant/components/unifi/.translations/hr.json new file mode 100644 index 00000000000..94a064f34b4 --- /dev/null +++ b/homeassistant/components/unifi/.translations/hr.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Host", + "password": "Lozinka", + "port": "Port", + "username": "Korisni\u010dko ime" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/hr.json b/homeassistant/components/upnp/.translations/hr.json new file mode 100644 index 00000000000..941f72f2e7d --- /dev/null +++ b/homeassistant/components/upnp/.translations/hr.json @@ -0,0 +1,9 @@ +{ + "config": { + "error": { + "few": "Nekoliko", + "one": "Jedan", + "other": "Ostalo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/.translations/es-419.json b/homeassistant/components/wemo/.translations/es-419.json new file mode 100644 index 00000000000..df390e73dd1 --- /dev/null +++ b/homeassistant/components/wemo/.translations/es-419.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos Wemo en la red.", + "single_instance_allowed": "Solo es posible una \u00fanica configuraci\u00f3n de Wemo." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar Wemo?", + "title": "Wemo" + } + }, + "title": "Wemo" + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/.translations/hr.json b/homeassistant/components/wemo/.translations/hr.json new file mode 100644 index 00000000000..389bfbd3cb1 --- /dev/null +++ b/homeassistant/components/wemo/.translations/hr.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Wemo" + } +} \ No newline at end of file diff --git a/homeassistant/components/wwlln/.translations/es-419.json b/homeassistant/components/wwlln/.translations/es-419.json new file mode 100644 index 00000000000..d185410a4ef --- /dev/null +++ b/homeassistant/components/wwlln/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Lugar ya registrado" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "radius": "Radio (usando su sistema de unidad base)" + }, + "title": "Complete su informaci\u00f3n de ubicaci\u00f3n." + } + }, + "title": "Red Mundial de Localizaci\u00f3n de Rayos (WWLLN)" + } +} \ No newline at end of file diff --git a/homeassistant/components/wwlln/.translations/hr.json b/homeassistant/components/wwlln/.translations/hr.json new file mode 100644 index 00000000000..09ca1a0273f --- /dev/null +++ b/homeassistant/components/wwlln/.translations/hr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Lokacija je ve\u0107 registrirana" + }, + "step": { + "user": { + "data": { + "latitude": "Zemljopisna \u0161irina", + "longitude": "Zemljopisna du\u017eina", + "radius": "Radius (koriste\u0107i sustav osnovne jedinice)" + }, + "title": "Ispunite podatke o lokaciji." + } + }, + "title": "Svjetska mre\u017ea lokacija munje (WWLLN)" + } +} \ No newline at end of file diff --git a/homeassistant/components/wwlln/.translations/pl.json b/homeassistant/components/wwlln/.translations/pl.json index d233b485bd0..704c7baeecb 100644 --- a/homeassistant/components/wwlln/.translations/pl.json +++ b/homeassistant/components/wwlln/.translations/pl.json @@ -8,11 +8,11 @@ "data": { "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna", - "radius": "Promie\u0144" + "radius": "Promie\u0144 (przy u\u017cyciu systemu jednostki bazowej)" }, - "title": "Wprowad\u017a dane o swojej lokalizacji." + "title": "Wpisz informacje o swojej lokalizacji." } }, - "title": "World Wide Lightning Location Network (WWLLN)" + "title": "\u015awiatowa sie\u0107 lokalizacji wy\u0142adowa\u0144 atmosferycznych (WWLLN)" } } \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/hr.json b/homeassistant/components/zone/.translations/hr.json new file mode 100644 index 00000000000..8a9f543be0a --- /dev/null +++ b/homeassistant/components/zone/.translations/hr.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Ime ve\u0107 postoji" + }, + "step": { + "init": { + "data": { + "icon": "Ikona", + "latitude": "Zemljopisna \u0161irina", + "longitude": "Zemljopisna du\u017eina", + "name": "Ime", + "passive": "Pasivno", + "radius": "Radijus" + }, + "title": "Definirajte parametre zone" + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/es-419.json b/homeassistant/components/zwave/.translations/es-419.json index 2e246fb9931..f2ca1a19aa4 100644 --- a/homeassistant/components/zwave/.translations/es-419.json +++ b/homeassistant/components/zwave/.translations/es-419.json @@ -4,6 +4,9 @@ "already_configured": "Z-Wave ya est\u00e1 configurado", "one_instance_only": "El componente solo admite una instancia de Z-Wave" }, + "error": { + "option_error": "La validaci\u00f3n de Z-Wave fall\u00f3. \u00bfEs correcta la ruta a la memoria USB?" + }, "step": { "user": { "data": { From 1e61d50fc52d6467565dde34b8d44905204a9093 Mon Sep 17 00:00:00 2001 From: Florian Klien Date: Tue, 27 Aug 2019 07:34:58 +0200 Subject: [PATCH 269/273] luci device-tracker dependency fix (#26215) * luci device-tracker dependency fix fixes issue #25758 * luci device-tracker fix, requirements_all --- homeassistant/components/luci/manifest.json | 3 ++- requirements_all.txt | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index 27b6a2da59f..153f6b5aea6 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -3,7 +3,8 @@ "name": "Luci", "documentation": "https://www.home-assistant.io/components/luci", "requirements": [ - "openwrt-luci-rpc==1.1.0" + "openwrt-luci-rpc==1.1.0", + "packaging==19.1" ], "dependencies": [], "codeowners": ["@fbradyirl"] diff --git a/requirements_all.txt b/requirements_all.txt index 42982b34134..10f3cb190c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -894,6 +894,9 @@ openwrt-luci-rpc==1.1.0 # homeassistant.components.orvibo orvibo==1.1.1 +# homeassistant.components.luci +packaging==19.1 + # homeassistant.components.mqtt # homeassistant.components.shiftr paho-mqtt==1.4.0 From d156648c55e82ab833f37130e5f2f21c7543a1c6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 27 Aug 2019 21:06:14 +0200 Subject: [PATCH 270/273] deCONZ normalizes cover values to follow zigbee spec (#26240) --- homeassistant/components/deconz/cover.py | 44 +++--------------------- tests/components/deconz/test_cover.py | 6 ++-- 2 files changed, 8 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index caa46e10f99..be4088a5c86 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -14,8 +14,6 @@ from .const import COVER_TYPES, DAMPERS, NEW_LIGHT, WINDOW_COVERS from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry -ZIGBEE_SPEC = ["lumi.curtain"] - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" @@ -35,13 +33,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] for light in lights: - if light.type in COVER_TYPES: - if light.modelid in ZIGBEE_SPEC: - entities.append(DeconzCoverZigbeeSpec(light, gateway)) - - else: - entities.append(DeconzCover(light, gateway)) + entities.append(DeconzCover(light, gateway)) async_add_entities(entities, True) @@ -69,14 +62,12 @@ class DeconzCover(DeconzDevice, CoverDevice): @property def current_cover_position(self): """Return the current position of the cover.""" - if self.is_closed: - return 0 - return int(self._device.brightness / 255 * 100) + return 100 - int(self._device.brightness / 255 * 100) @property def is_closed(self): """Return if the cover is closed.""" - return not self._device.state + return self._device.state @property def device_class(self): @@ -96,9 +87,9 @@ class DeconzCover(DeconzDevice, CoverDevice): position = kwargs[ATTR_POSITION] data = {"on": False} - if position > 0: + if position < 100: data["on"] = True - data["bri"] = int(position / 100 * 255) + data["bri"] = 255 - int(position / 100 * 255) await self._device.async_set_state(data) @@ -116,28 +107,3 @@ class DeconzCover(DeconzDevice, CoverDevice): """Stop cover.""" data = {"bri_inc": 0} await self._device.async_set_state(data) - - -class DeconzCoverZigbeeSpec(DeconzCover): - """Zigbee spec is the inverse of how deCONZ normally reports attributes.""" - - @property - def current_cover_position(self): - """Return the current position of the cover.""" - return 100 - int(self._device.brightness / 255 * 100) - - @property - def is_closed(self): - """Return if the cover is closed.""" - return self._device.state - - async def async_set_cover_position(self, **kwargs): - """Move the cover to a specific position.""" - position = kwargs[ATTR_POSITION] - data = {"on": False} - - if position < 100: - data["on"] = True - data["bri"] = 255 - int(position / 100 * 255) - - await self._device.async_set_state(data) diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index f264877b77a..7230ff4fb7b 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -16,7 +16,7 @@ SUPPORTED_COVERS = { "id": "Cover 1 id", "name": "Cover 1 name", "type": "Level controllable output", - "state": {"bri": 255, "reachable": True}, + "state": {"bri": 255, "on": False, "reachable": True}, "modelid": "Not zigbee spec", "uniqueid": "00:00:00:00:00:00:00:00-00", }, @@ -24,7 +24,7 @@ SUPPORTED_COVERS = { "id": "Cover 2 id", "name": "Cover 2 name", "type": "Window covering device", - "state": {"bri": 255, "reachable": True}, + "state": {"bri": 255, "on": True, "reachable": True}, "modelid": "lumi.curtain", }, } @@ -107,7 +107,7 @@ async def test_cover(hass): cover_1 = hass.states.get("cover.cover_1_name") assert cover_1 is not None - assert cover_1.state == "closed" + assert cover_1.state == "open" gateway.api.lights["1"].async_update({}) From bbc50498163391e50f3f98e395f64a893d1f34fc Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Wed, 28 Aug 2019 09:21:21 +0200 Subject: [PATCH 271/273] SMA beta fix #26225 (#26244) --- homeassistant/components/sma/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index b2692a37059..34aed146cf0 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -143,7 +143,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= hass_sensors.append(SMAsensor(sensor_def[name], sub_sensors)) used_sensors.append(name) used_sensors.extend(attr) - used_sensors = [sensor_def[s] for s in set(used_sensors)] if isinstance(config_sensors, list): if not config_sensors: # Use all sensors by default @@ -152,6 +151,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= for sensor in used_sensors: hass_sensors.append(SMAsensor(sensor_def[sensor], [])) + used_sensors = [sensor_def[s] for s in set(used_sensors)] async_add_entities(hass_sensors) # Init the SMA interface From 1c473487b18cef3660d32d1380ca79ca725501a3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Aug 2019 13:38:56 -0700 Subject: [PATCH 272/273] Bumped version to 0.98.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4d2998b85b8..9a9b098aabb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 98 -PATCH_VERSION = "0b2" +PATCH_VERSION = "0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From 69ddca6f68415ebaa32f3e2e89f9d84d1d05b5e4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Aug 2019 13:43:45 -0700 Subject: [PATCH 273/273] Updated frontend to 20190828.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 78f87639a99..fa6145a7af2 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==20190825.0" + "home-assistant-frontend==20190828.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 873a5aaf31d..a1ffd515c5b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.17 -home-assistant-frontend==20190825.0 +home-assistant-frontend==20190828.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 10f3cb190c6..e749340a8cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -624,7 +624,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190825.0 +home-assistant-frontend==20190828.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 96b82caf968..1aad0450390 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -176,7 +176,7 @@ hdate==0.9.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190825.0 +home-assistant-frontend==20190828.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0